I have a mern application that allows users to report match results. I use a modal to create the result. Once the report is entered the data ir refetched on the compeitor page and the report shows up in the list.
If I edit the report it changes in the database and the data even changes in the report list, but if I click the edit button again without refreshing the page, the modal still shows the old data.
I thought maybe the modal was the issue so for the editReport, I tried it on a regular page but got the same result. I use refetch in the onSubmit which is passed from the competition so that data is refetched and updated when the modal closes, but it still doesn’t help with the data for the modal.
Here is the competitor page that lists the records with the edit button to open the modal.
import React, { useState } from "react";
import moment from "moment";
import Loader from "../Loader";
import { Button, Col, Row, Table } from "react-bootstrap";
import { useGetAthleteMatchReportsQuery } from "../../slices/reportsApiSlice";
import { useGetMyTeamsQuery } from "../../slices/athleteApiSlice";
import AddReportModal from "../AddReportModal";
// icons
import { GrEdit } from "react-icons/gr";
import EditReportModal from "../EditReportModal";
const CompetitionInfo = ({ user }) => {
// const [showMatchModal, setShowMatchModal] = useState(false);
// const handleMatchShow = () => setShowMatchModal(true);
// const handleMatchClose = () => setShowMatchModal(false);
const [showReportModal, setShowReportModal] = useState(false);
const handleReportShow = () => setShowReportModal(true);
const handleReportClose = () => setShowReportModal(false);
const [showEditReportModal, setShowEditReportModal] = useState(false);
const handleEditReportShow = () => setShowEditReportModal(true);
const handleEditReportClose = () => setShowEditReportModal(false);
const [editReportId, setEditReportId] = useState("");
const handleEditClick = (reportId) => {
setEditReportId(reportId);
};
const {
data: myMatchReports,
isLoading,
refetch,
} = useGetAthleteMatchReportsQuery(user.athlete);
const {
data: myTeams,
isLoading: teamLoading,
refetch: teamsRefetch,
} = useGetMyTeamsQuery(user.athlete);
!teamLoading && console.log("my teams ", myTeams);
!isLoading && console.log("match reports ", myMatchReports);
return (
<div>
<h1>My Competition Info</h1>
<Button onClick={handleReportShow} className="basic_btn my-3">
Add a Match
</Button>
<br />
{isLoading && <Loader />}
<h2>Table for match results from reports table</h2>
<Table striped bordered hover responsive>
<thead>
<tr>
<th className="align-items-center">Match Type</th>
<th className="align-items-center">Event Name</th>
<th className="align-items-center">Event Date</th>
<th className="align-items-center">Opponent Name</th>
<th className="align-items-center">Result</th>
<th className="align-items-center">Edit</th>
</tr>
</thead>
<tbody>
{myMatchReports && myMatchReports.length > 0 ? (
myMatchReports.map(
(report) =>
report.reportType === "Result" && (
<tr key={report._id}>
<td className="table_bold_text">{report.reportType}</td>
<td>
{console.log(report.eventEndDate)}
{report.eventName.length > 25
? `${report.eventName.slice(0, 25)}...`
: report.eventName}
</td>
<td className="table_bold_text">
{report.eventStartDate && report.eventEndDate
? `${moment(report.eventStartDate).format(
"M-D-YYYY"
)} - ${moment(report.eventEndDate).format(
"M-D-YYYY"
)}`
: report.eventStartDate
? moment(report.eventStartDate).format("M-D-YYYY")
: "No date"}
</td>
<td className="table_bold_text">{report.opponentName}</td>
<td className="table_bold_text">{report.result}</td>
<td className="table_bold_text">
<GrEdit
type="button"
onClick={() => {
handleEditClick(report._id);
handleEditReportShow();
}}
/>
</td>
</tr>
)
)
) : (
<tr>
<td colSpan="4">
No matches found
<Button onClick={handleReportShow} className="basic_btn my-3">
Add a Match
</Button>
</td>
</tr>
)}
</tbody>
</Table>
<Row className="py-3">
<Col>
<AddReportModal
show={showReportModal}
handleClose={handleReportClose}
athleteId={user.athlete}
myTeams={myTeams}
refetch={refetch}
/>
</Col>
</Row>
<Row className="py-3">
<Col>
<EditReportModal
show={showEditReportModal}
handleClose={handleEditReportClose}
editReportId={editReportId}
refetch={refetch}
/>
</Col>
</Row>
</div>
);
};
export default CompetitionInfo;
Here is the EditReportModal:
import { useEffect, useRef, useState } from "react";
import { Button, Modal, Row, Col } from "react-bootstrap";
import FormContainer from "./FormContainer";
import { Form, Formik } from "formik";
import { Tooltip } from "react-tooltip";
import Countries from "../assets/countries.json";
import {
useGetReportByIdQuery,
useUpdateReportMutation,
} from "../slices/reportsApiSlice";
import { createReport } from "../schemas";
import moment from "moment";
import CustomRadio from "./inputs/CustomRadio";
import CustomSelect from "./inputs/CustomSelect";
import CustomInput from "./inputs/CustomInput";
import CustomDatePicker from "./inputs/CustomDatePicker";
import CustomTextarea from "./inputs/CustomTextarea";
import CustomCheckbox from "./inputs/CustomCheckbox";
import { toast } from "react-toastify";
import Loader from "./Loader";
// icons
import { BsFillQuestionDiamondFill } from "react-icons/bs";
const EditReportModal = ({ show, handleClose, editReportId, refetch }) => {
const { data: myReport, isLoading } = useGetReportByIdQuery(editReportId);
const [athlete, setAthlete] = useState("");
const [reportType, setReportType] = useState("");
const [matchType, setMatchType] = useState("");
const [eventName, setEventName] = useState("");
const [eventStartDate, setEventStartDate] = useState("");
const [eventEndDate, setEventEndtDate] = useState("");
const [opponentName, setOpponentName] = useState("");
const [weightCategory, setWeightCategory] = useState("");
const [opponentClub, setOpponentClub] = useState("");
const [opponentCountry, setOpponentCountry] = useState("");
const [opponentRank, setOpponentRank] = useState("");
const [opponentHandedness, setOpponentHandedness] = useState("");
const [opponentAttacks, setOpponentAttacks] = useState("");
const [opponentAttackNotes, setOpponentAttackNotes] = useState("");
const [athleteAttacks, setAthleteAttacks] = useState("");
const [athleteAttackNotes, setAthleteAttackNotes] = useState("");
const [result, setResult] = useState("");
const [score, setScore] = useState("");
const [isPublic, setIsPublic] = useState("");
useEffect(() => {
if (myReport) {
setReportType(myReport?.reportType);
setMatchType(myReport?.matchType);
setEventName(myReport?.eventName);
setWeightCategory(myReport?.weightCategory);
setEventStartDate(myReport?.eventStartDate);
setEventEndtDate(myReport?.eventEndDate);
setOpponentName(myReport?.opponentName);
setOpponentClub(myReport?.opponentClub);
setOpponentCountry(myReport?.opponentCountry);
setOpponentRank(myReport?.opponentRank);
setOpponentHandedness(myReport?.opponentHandedness);
setOpponentAttacks(myReport?.opponentAttacks);
setOpponentAttackNotes(myReport?.opponentAttackNotes);
setAthleteAttacks(myReport?.athleteAttacks);
setAthleteAttackNotes(myReport?.athleteAttackNotes);
setResult(myReport?.result);
setScore(myReport?.score);
setIsPublic(myReport?.isPublic);
}
}, [myReport]);
console.log("line 74 result ", result);
const [updateReport, { isLoading: updateLoading }] =
useUpdateReportMutation();
const onSubmit = async (values, action) => {
try {
const res = await updateReport({
editReportId,
reportType: reportType,
matchType: values.matchType,
eventName: values.eventName,
eventStartDate: values.eventStartDate,
eventEndDate: values.eventEndDate,
opponentName: values.opponentName,
weightCategory: values.weightCategory,
opponentClub: values.opponentClub,
opponentCountry: values.opponentCountry,
opponentRank: values.opponentRank,
opponentHandedness: values.opponentHandedness,
opponentAttacks: values.opponentAttacks,
opponentAttackNotes: values.opponentAttackNotes,
athleteAttacks: values.athleteAttacks,
athleteAttackNotes: values.athleteAttackNotes,
result: values.result,
score: values.score,
isPublic: values.isPublic,
}).unwrap();
refetch();
handleClose();
} catch (err) {
toast.error(err?.data?.message || err.message);
}
};
const windowSize = useRef([window.innerWidth, window.innerHeight]);
const tooltipWidth = windowSize.current[0] > 500 ? "50vw" : "90vw";
console.log("result ", result);
return (
<Modal size="xl" show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Add Match</Modal.Title>
</Modal.Header>
<Modal.Body>
{isLoading && <Loader />}
<FormContainer>
<Formik
enableReinitialize={true}
initialValues={{
//athlete: "",
reportType: reportType,
matchType: matchType,
eventName: eventName,
eventStartDate: "",
eventEndDate: "",
opponentName: opponentName,
weightCategory: weightCategory,
opponentClub: opponentClub,
opponentCountry: opponentCountry,
opponentRank: opponentRank,
opponentHandedness: opponentHandedness,
opponentAttacks: opponentAttacks,
opponentAttackNotes: opponentAttackNotes,
athleteAttacks: athleteAttacks,
athleteAttackNotes: athleteAttackNotes,
result: String(result),
score: score,
isPublic: isPublic,
}}
validateSchema={createReport}
onSubmit={onSubmit}
>
{({ isSubmitting }) => (
<Form>
<Row>
<Col className="d-flex flex-column my-2">
<label>
Report Type
<BsFillQuestionDiamondFill
data-tooltip-id="report-type-tooltip"
className="ms-2"
/>
<Tooltip
id="report-type-tooltip"
style={{
width: `${tooltipWidth}`,
zIndex: "9999",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
Select Result if you only want to record match
results.
<br />
Select Scouting if you are scouting an athlete for
future matches
<br />
Select both if you want to record results and scout
the opponent for futuer matches.
</div>
</Tooltip>
</label>
<div className="d-flex flex-column align-items-start">
<CustomRadio
label="Result"
value="Result"
name="reportType"
placeholder="Result"
checked={reportType === "Result"}
/>
<CustomRadio
label="Scouting"
value="Scouting"
name="reportType"
placeholder="Scouting"
checked={reportType === "Scouting"}
/>
<CustomRadio
label="Both"
value="Both"
name="reportType"
placeholder="Both"
checked={reportType === "Both"}
/>
</div>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-2">
<CustomSelect
label="Match Type"
name="matchType"
placeholder="Select match type..."
value={matchType}
>
<option value="">Select event type...</option>
<option value="BJJ">Brazilian Jiu Jitsu</option>
<option value="Judo">Judo</option>
<option value="Wrestling">Wrestling</option>
</CustomSelect>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-2">
<CustomInput
label="Event Name"
name="eventName"
type="text"
placeholder="Enter event name"
/>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-2">
<CustomDatePicker
label="Event Start Date"
name="eventStartDate"
/>
</Col>
<Col className="d-flex flex-column my-2">
<CustomDatePicker
label="Event End Date"
name="eventEndDate"
/>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-2">
<CustomInput
label="Weight Category"
name="weightCategory"
type="text"
/>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-2">
<CustomInput
label="Opponent Name"
name="opponentName"
type="text"
/>
</Col>
<Col className="d-flex flex-column my-2">
<CustomInput
label="Opponent Club"
name="opponentClub"
type="text"
/>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-3">
<CustomInput
label="Opponent Rank"
name="opponentRank"
type="text"
/>
</Col>
<Col className="d-flex flex-column my-3">
<label>Primarily right or left handed</label>
<div className="d-flex flex-column align-items-start">
<CustomRadio
label="Righty"
value="righty"
name="opponentHandedness"
/>
<CustomRadio
label="Lefty"
value="lefty"
name="opponentHandedness"
/>
</div>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-2">
<CustomSelect
label="Country"
name="opponentCountry"
placeholder="Select country..."
value={opponentCountry}
>
<option value="">Select country...</option>
<option value="">----------</option>
{Countries.map((country) => (
<option key={country.code3} value={country.code3}>
{country.name}
</option>
))}
</CustomSelect>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-3">
<CustomTextarea
label="Opponents attacks"
type="textarea"
name="opponentAttacks"
placeholder="Enter opponent's attacks"
/>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-3">
<CustomTextarea
label="Notes on opponents attacks"
type="textarea"
name="opponentAttackNotes"
placeholder="Enter notes on opponent's attacks"
/>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-3">
<CustomTextarea
label="My/My Athlete's attacks"
type="textarea"
name="athleteAttacks"
placeholder="Enter My/My Athlete's attacks"
/>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-3">
<CustomTextarea
label="Notes on my/my athlete's attacks"
type="textarea"
name="athleteAttackNotes"
placeholder="Enter notes on opponent's attacks"
/>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-3">
<label>Outcome</label>
<div className="d-flex flex-column align-items-start">
<CustomRadio label="Won" value="Won" name="result" />
<CustomRadio label="Lost" value="Lost" name="result" />
</div>
</Col>
<Col className="d-flex flex-column my-3">
<CustomInput label="Score" name="score" type="text" />
</Col>
</Row>
<Row>
<Col className="d-flex flex-column my-3">
<CustomCheckbox
type="checkbox"
text="Make this report public"
name="isPublic"
tooltipText="Make this report public"
/>
</Col>
</Row>
<Row>
<Col className="d-flex flex-column align-items-center gap-2">
<Button
disabled={isSubmitting}
type="submit"
className="login_btn"
>
Add Report
</Button>
</Col>
</Row>
</Form>
)}
</Formik>
</FormContainer>
</Modal.Body>
</Modal>
);
};
export default EditReportModal;
2
Answers
I don't know if this is the best way to handle this, but I was able to get it to work by refetching when the modal loads. I renamed refetch to localRefetch because I have refetch being pass to the modal to refetch the data on the competitor page when the form is submitted from the modal.
Again, I'm not sure if it's the best practice, but it seems to be working.
I have tried to replicate your use case of the edit form. Your use case is that your form 1st initialized with stale/dummy data, and once the API call is successful, it shows the real data. I suspect that you don’t need useState to store each value. Will still try to solve with and without it.
Method 1: Don’t show the form until the data is loaded.
Benefits:
Method 2: Make the form values controlled values.
The form only takes the initial values from initialValues props. It then loads the values that it gets from useState values. Also onChange needs to be added for each input type.
You can check both the methods here: https://codesandbox.io/p/sandbox/react-test-forked-f9d3cl