We have this component. How to get notified when component unmounts or disappears?
function EventTime(props) {
const match = useRouteMatch();
const eventTime = findEventTime(match.params.eventTimeId);
const eventTimeId = match.params.eventTimeId;
const pageId = getId(undefined, eventTimeId, true, false);
const eventId = getId(undefined, eventTimeId, false, true);
const [
enableSellingTicketForEventTime,
setEnableSellingTicketForEventTime
] = useState(eventTime.enableSellingTicketForEventTime);
const [additionalNotesOnMyTicket, setAdditionalNotesOnMyTicket] = useState(
""
);
const changeAdditionalNotesOnMyTicket = event => {
setAdditionalNotesOnMyTicket(event.target.value);
};
const handleSwitchItem = () => {
setEnableSellingTicketForEventTime(!enableSellingTicketForEventTime);
};
const updateAdditionalNotesOnMyTicket = () => {
update({
pageId,
eventId,
eventTimeId,
additionalNotesOnMyTicket
}).then(data => {
const managedPages = JSON.parse(localStorage.getItem("managedPages"));
const index = managedPages.findIndex(
managedPage => managedPage.id === pageId
);
managedPages[index].events[eventId].eventTimes[
eventTimeId
].additionalNotesOnMyTicket = additionalNotesOnMyTicket;
localStorage.setItem("managedPages", JSON.stringify(managedPages));
console.log(JSON.parse(localStorage.getItem("managedPages")));
});
};
console.log(eventTime);
return (
<div>
<List>
{page().permission === "admin" && (
<Link
to={`/eventTimeLocationSelector/${match.params.eventTimeId}`}
style={{ textDecoration: "none", color: "black" }}
>
<NavigationItem
primary="Location"
secondary={
eventTime.locationId
? getLocationName(eventTime.locationId)
: ""
}
/>
</Link>
)}
{page().permission === "admin" && page().auditoriums && (
<Link
to={`/eventTimeAuditoriumSelector/${match.params.eventTimeId}`}
style={{ textDecoration: "none", color: "black" }}
>
<NavigationItem
primary="Auditorium"
secondary={
eventTime.auditoriumId
? getAuditoriumName(eventTime.auditoriumId)
: ""
}
/>
</Link>
)}
{page().permission !== "validateTicket" && (
<SwitchItem
primary="Enable selling ticket for event time"
checked={enableSellingTicketForEventTime}
change={handleSwitchItem}
default={false}
/>
)}
<Link
to={`/eventTimeTransactions/${match.params.eventTimeId}`}
style={{ textDecoration: "none", color: "black" }}
>
<NavigationItem primary="Sold tickets" />
</Link>
<TextFieldItem
primary="Additional notes on my ticket"
value={additionalNotesOnMyTicket}
onChange={changeAdditionalNotesOnMyTicket}
onBlur={updateAdditionalNotesOnMyTicket}
/>
</List>
{page().permission !== "validateTicket" &&
page().permission !== "teacher" && (
<Box style={{ display: "flex", justifyContent: "flex-end" }} mr={2}>
<Button variant="contained" color="primary">
Add person
</Button>
</Box>
)}
As you are using hooks you can use useEffect hook.
import {useEffect} from 'react';
Now, inside you component;
useEffect(() => {
return () => {
// Do something here when component unmounts
}
}, [])
Check more about useEffect here: https://reactjs.org/docs/hooks-effect.html
You can use like this.
You can declare a function to be called when the component unmounts as the useEffect()'s hook return value. Check out this: useEffect hook official docs
Related
I have a react component Matches.js that handles the output of tournament matches, sorted by their rounds. All the data is fetched from a parent component which fetches it via nextjs SSR and then gives the data as props to the child component. To avoid additional requests via page reloads based on data changes (new, update, delete), I'm trying to update the output via a state array matches, which is updated by child components if the backend request worked. It works flawlessly with add and delete operations, but updating gives me serious headaches. Only updating a match item doesn't rerender the match output at all.
For displaying the matches, I use the const displayMatches which maps matches according to their rounds, so two .map() functions. I've pinpointed the problem to the keys which react demands as unique props. When they don't change, displayMatches doesn't rerender with the updated data. I'm using match._id as unique values for the keys which always stay the same. I tried randomizing the the id a bit, but the results were weird and wonky at best.
How may I trigger a rerender with the updated values after an update operations? I'd like to avoid going the 'easy way' by just forcing the page to reload, which works fine.
The data of the state array is clearly updated on time, as seen in console.logs. The array consists of objects like this one:
{
tournamentId: tournamentId,
_id: id,
encho: encho,
round: round,
red: {
name: redName,
ippon: redIppon,
hansoku: redHansoku,
winByHantei: redWinByHantei
},
white: {
name: whiteName,
ippon: whiteIppon,
hansoku: whiteHansoku,
winByHantei: whiteWinByHantei
}
Matches.js:
import { useState } from "react"
import SingleMatchView from "./SingleMatchView"
import SingleMatchEdit from "./SingleMatchEdit"
import { roundMap, refreshPage } from "../../store/helpers"
import Button from "#mui/material/Button"
import Stack from "#mui/material/Stack"
import RefreshIcon from "#mui/icons-material/Refresh"
export default function Matches({
matches: matchData,
isLoggedIn,
tournamentId
}) {
const [matches, setMatches] = useState(matchData)
const [sortASC, setSortASC] = useState(true)
const addMatchToState = (match) => {
return setMatches((prev) => [...prev, match])
}
const updateMatchInState = (updateData) => {
setMatches((prev) => {
return prev.map((match) => {
if (updateData._id === match._id) {
return {
...match,
...updateData
}
}
return match
})
})
}
const deleteMatchInState = (matchId) => {
return setMatches((prev) => {
return prev.filter((match) => {
return match._id != matchId
})
})
}
const uniqueRounds = [...new Set(matches?.map((match) => match.round))]
const displayMatches = uniqueRounds.map((round) => {
return (
<div key={round}>
<h2>{roundMap[round]}</h2>
{matches
.filter((match) => match.round === round)
.map((match) => (
<SingleMatchView
key={match._id}
tournamentId={tournamentId}
matchData={match}
isLoggedIn={isLoggedIn}
deleteMatchInState={deleteMatchInState}
updateMatchInState={updateMatchInState}
/>
))}
</div>
)
})
console.log("Matches rerendered")
return (
<>
<Stack direction="row" spacing={2} my={2} justifyContent={"center"}>
<Button
onClick={() => setSortASC((prev) => !prev)}
variant="contained"
color="secondary"
>
{sortASC ? "Sort Pool ➝ Final" : "Sort Final ➝ Pool"}
</Button>
<Button
onClick={refreshPage}
variant="contained"
startIcon={<RefreshIcon />}
color="secondary"
>
Refresh
</Button>
</Stack>
{isLoggedIn && (
<SingleMatchEdit
addMatchToState={addMatchToState}
isNew={true}
tournamentId={tournamentId}
/>
)}
{matches.length === 0 && "No matches yet"}
{sortASC ? displayMatches : displayMatches.reverse()}
</>
)
}
/edit: additional code
SingleMatchView.js
import { useState } from "react"
import { roundMap } from "../../store/helpers"
import SingleMatchEdit from "./SingleMatchEdit"
import { httpDeleteIndividualMatch } from "../../hooks/requests"
import Stack from "#mui/material/Stack"
import Button from "#mui/material/Button"
import DeleteIcon from "#mui/icons-material/Delete"
import EditIcon from "#mui/icons-material/Edit"
import ClearIcon from "#mui/icons-material/Clear"
export default function SingleMatchView({
tournamentId,
matchData,
isLoggedIn,
updateMatchInState,
deleteMatchInState
}) {
const hansokuIcon = "▲"
const [editMode, setEditMode] = useState(false)
const [confirmDelete, setConfirmDelete] = useState(false)
const [encho, setEncho] = useState(matchData?.encho || false)
const [round, setRound] = useState(
matchData?.round || Object.keys(roundMap)[0]
)
const [redName, setRedName] = useState(matchData?.red?.name || "")
const [redIppon, setRedIppon] = useState(matchData?.red?.ippon || "")
const [redHansoku, setRedHansoku] = useState(matchData?.red?.hansoku || 0)
const [redWinByHantei, setRedWinByHantei] = useState(
matchData?.red?.winByHantei || false
)
const [whiteName, setWhiteName] = useState(matchData?.white?.name || "")
const [whiteIppon, setWhiteIppon] = useState(matchData?.white?.ippon || "")
const [whiteHansoku, setWhiteHansoku] = useState(
matchData?.white?.hansoku || 0
)
const [whiteWinByHantei, setWhiteWinByHantei] = useState(
matchData?.white?.winByHantei || false
)
const redPoints = redIppon.length + Math.floor(whiteHansoku / 2)
const whitePoints = whiteIppon.length + Math.floor(redHansoku / 2)
const redIpponIcons = redIppon?.split("").map((ippon, i) => {
return (
<span key={i} className="ippon-icon">
{ippon}
</span>
)
})
const whiteIpponIcons = whiteIppon?.split("").map((ippon, i) => {
return (
<span key={i} className="ippon-icon">
{ippon}
</span>
)
})
const isHikiWake = redPoints === whitePoints
// const dataObject = {
// encho: encho,
// round: round,
// red: {
// name: redName.trim(),
// ippon: redIppon,
// hansoku: redHansoku,
// winByHantei: redWinByHantei
// },
// white: {
// name: whiteName.trim(),
// ippon: whiteIppon,
// hansoku: whiteHansoku,
// winByHantei: whiteWinByHantei
// }
// }
let deleteTimer
const prepareDeleteFight = async () => {
setConfirmDelete(true)
deleteTimer = setTimeout(() => {
setConfirmDelete(false)
}, 5000)
}
const confirmDeleteFight = async () => {
try {
const response = await httpDeleteIndividualMatch(matchData._id)
if (response.acknowledged) {
return deleteMatchInState(matchData._id)
}
console.log("Error, match not deleted")
return
} catch (err) {
console.log(err)
} finally {
setConfirmDelete(false)
clearTimeout(deleteTimer)
}
}
return (
<>
<div className="display">
{/* {matchData._id} */}
{/* {round && <div className="round">{round}</div>} */}
<div className="board-single-results">
<div className="red-color stripe"></div>
<div
className="red-name"
// Warning: Prop `style` did not match. Server: "null" Client: "background-color:"
// style={{
// "background-color":
// redPoints > whitePoints ? "gold" : ""
// }}
>
{redName.toUpperCase() || "???"}
</div>
<div className="hansoku red-hansoku">
{hansokuIcon.repeat(redHansoku)}
</div>
<div className="ippon red-ippon">
{redIppon && redIpponIcons.reverse()}
</div>
<div className="points red-points">{redPoints}</div>
<div className="win-modifier">
{
// to do: Encho geht nicht gleichzeitig mit Hikiwake, check einfügen
}
{encho && !isHikiWake && "E"}
{!encho && isHikiWake && <ClearIcon />}
</div>
<div className="points white-points">{whitePoints}</div>
<div className="ippon white-ippon">
{whiteIppon && whiteIpponIcons}
</div>
<div className="hansoku white-hansoku">
{hansokuIcon.repeat(whiteHansoku)}
</div>
<div
className="white-name"
// Warning: Prop `style` did not match. Server: "null" Client: "background-color:"
// style={{
// "background-color":
// whitePoints > redPoints ? "gold" : ""
// }}
>
{whiteName.toUpperCase()}
</div>
<div className="white-color stripe"></div>
</div>
{isLoggedIn && (
<div className="toolbox">
<Stack direction="row" spacing={2}>
<Button
onClick={() => setEditMode((prev) => !prev)}
variant="contained"
startIcon={<EditIcon />}
size="small"
>
EDIT
</Button>
{!confirmDelete && (
<Button
onClick={prepareDeleteFight}
color="error"
variant="contained"
startIcon={<DeleteIcon />}
size="small"
>
Delete
</Button>
)}
{confirmDelete && (
<Button
onClick={confirmDeleteFight}
color="error"
variant="contained"
startIcon={<DeleteIcon />}
size="small"
>
Confirm Deletion
</Button>
)}
</Stack>
</div>
)}
</div>
{editMode && (
<SingleMatchEdit
matchData={matchData}
tournamentId={tournamentId}
setEditMode={setEditMode}
updateMatchInState={updateMatchInState}
/>
)}
</>
)
}
SingleMatchEdit.js (handleSubmitUpdate being the important part here)
export default function SingleMatchEdit({
matchData,
isNew,
tournamentId,
setEditMode,
updateMatchInState,
addMatchToState
}) {
const matchId = matchData?._id
const [encho, setEncho] = useState(matchData?.encho || false)
const [round, setRound] = useState(
matchData?.round || Object.keys(roundMap)[0]
)
const [redName, setRedName] = useState(matchData?.red?.name || "")
const [redIppon, setRedIppon] = useState(matchData?.red?.ippon || "")
const [redHansoku, setRedHansoku] = useState(matchData?.red?.hansoku || 0)
const [redWinByHantei, setRedWinByHantei] = useState(
matchData?.red?.winByHantei || false
)
const [whiteName, setWhiteName] = useState(matchData?.white?.name || "")
const [whiteIppon, setWhiteIppon] = useState(matchData?.white?.ippon || "")
const [whiteHansoku, setWhiteHansoku] = useState(
matchData?.white?.hansoku || 0
)
const [whiteWinByHantei, setWhiteWinByHantei] = useState(
matchData?.white?.winByHantei || false
)
const resetMatchData = () => {
setEncho(false)
setRound(Object.keys(roundMap)[0])
setRedName("")
setRedIppon("")
setRedHansoku(0)
setRedWinByHantei(false)
setWhiteName("")
setWhiteIppon("")
setWhiteHansoku(0)
setWhiteWinByHantei(false)
}
const addRedIppon = (e) => {
if (redIppon.length >= 2) return
setRedIppon((prev) => prev.concat(e.target.name))
}
const remRedIppon = () => setRedIppon("")
const addWhiteIppon = (e) => {
if (whiteIppon.length >= 2) return
setWhiteIppon((prev) => prev.concat(e.target.name))
}
const remWhiteIppon = () => setWhiteIppon("")
const redIpponButtons = ipponButtons(ipponMap, addRedIppon, "red")
const whiteIpponButtons = ipponButtons(ipponMap, addWhiteIppon, "white")
const matchDataToSend = {
tournamentId: tournamentId,
encho: encho,
round: round,
red: {
name: redName.trim(),
ippon: redIppon,
hansoku: redHansoku,
winByHantei: redWinByHantei
},
white: {
name: whiteName.trim(),
ippon: whiteIppon,
hansoku: whiteHansoku,
winByHantei: whiteWinByHantei
}
}
const handleSubmitNew = async (e) => {
e.preventDefault()
try {
const response = await httpSubmitMatch(matchDataToSend)
if (response.acknowledged) {
addMatchToState({
...matchDataToSend,
_id: response.insertedId
})
resetMatchData()
}
if (!response.acknowledged) {
// do something
throw new Error("Data not acklowleged!")
}
} catch (err) {
console.log(err)
}
}
const handleSubmitUpdate = async (e) => {
e.preventDefault()
try {
const response = await httpUpdateIndividualMatch(
matchId,
matchDataToSend
)
if (response.acknowledged) {
// To Do: Update State
updateMatchInState({ ...matchDataToSend, _id: matchId })
setEditMode(false)
}
if (!response.acknowledged) {
// do something
throw new Error("Data not acklowleged!")
}
} catch (err) {
console.log(err)
}
}
function removeRedHansoku() {
if (redHansoku <= 0) return
setRedHansoku((prev) => prev - 1)
}
function addRedHansoku() {
if (redHansoku >= 4) return
setRedHansoku((prev) => prev + 1)
}
function removeWhiteHansoku() {
if (whiteHansoku <= 0) return
setWhiteHansoku((prev) => prev - 1)
}
function addWhiteHansoku() {
if (whiteHansoku >= 4) return
setWhiteHansoku((prev) => prev + 1)
}
const rounds = Object.keys(roundMap).map((round) => {
return (
// <option key={round} value={round}>
// {roundMap[round]}
// </option>
<MenuItem key={round} value={round}>
{roundMap[round]}
</MenuItem>
)
})
return (
<div className={styles["edit-board"]}>
<div className={styles["board"]}>
<div className={styles["red-color"]}></div>
<div className={styles["red-name"]}>
{/* Replace with Autocomplete and a list of all names that were entered in the past, retrieved from DB */}
{/* <TextField
fullWidth
id="outlined-basic"
label="Name"
variant="outlined"
size="small"
margin="normal"
value={redName}
onChange={(e) => setRedName(e.target.value)}
/> */}
<AutocompletePlayerName
value={redName}
setNameFunc={setRedName}
/>
</div>
<div className={styles["white-name"]}>
{/* Replace with Autocomplete and a list of all names that were entered in the past, retrieved from DB */}
{/* <TextField
fullWidth
id="outlined-basic"
label="Name"
variant="outlined"
size="small"
margin="normal"
value={whiteName}
onChange={(e) => setWhiteName(e.target.value)}
/> */}
<AutocompletePlayerName
value={whiteName}
setNameFunc={setWhiteName}
/>
</div>
<div className={styles["white-color"]}></div>
<div className={`${styles.ippon} ${styles["red-ippon"]}`}>
<div className={styles["awarded-ippon"]}>
{redIppon ? `➝${redIppon}` : "(IPPON)"}
</div>
<div className={styles["add-ippon-icon-table"]}>
{redIpponButtons}
</div>
<Button
color="warning"
variant="contained"
startIcon={<CancelOutlinedIcon />}
size="small"
onClick={remRedIppon}
>
Reset Ippon
</Button>
</div>
<div className={`${styles.ippon} ${styles["white-ippon"]}`}>
<div className={styles["awarded-ippon"]}>
{whiteIppon ? `➝${whiteIppon}` : "(IPPON)"}
</div>
<div className={styles["add-ippon-icon-table"]}>
{whiteIpponButtons}
</div>
<Button
color="warning"
variant="contained"
startIcon={<CancelOutlinedIcon />}
size="small"
onClick={remWhiteIppon}
>
Reset Ippon
</Button>
</div>
<div
className={styles["red-hansoku"]}
style={{ justifyContent: "space-between" }}
>
<IconButton
onClick={removeRedHansoku}
sx={{ color: "#00000078" }}
>
<RemoveCircleIcon />
</IconButton>
{redHansoku ? "▲".repeat(redHansoku) : "(HANSOKU)"}
<IconButton
onClick={addRedHansoku}
sx={{ color: "#00000078" }}
>
<AddCircleIcon />
</IconButton>
</div>
<div
className={styles["white-hansoku"]}
style={{ justifyContent: "space-between" }}
>
<IconButton
onClick={removeWhiteHansoku}
sx={{ color: "#00000078" }}
>
<RemoveCircleIcon />
</IconButton>
{whiteHansoku ? "▲".repeat(whiteHansoku) : "(HANSOKU)"}
<IconButton
onClick={addWhiteHansoku}
sx={{ color: "#00000078" }}
>
<AddCircleIcon />
</IconButton>
</div>
<div className={styles["additional-information"]}>
<FormControl variant="standard" size="small">
<Select
labelId=""
id=""
value={round}
label="Round"
onChange={(e) => setRound(e.target.value)}
>
{rounds}
</Select>
{/* <select
name="round"
value={round}
onChange={(e) => setRound(e.target.value)}
>
{rounds}
</select> */}
<FormControlLabel
control={
<Checkbox
checked={encho}
onChange={() => setEncho((prev) => !prev)}
/>
}
label="Encho"
/>
</FormControl>
{/* <label>
<input
type="checkbox"
defaultChecked={encho}
onChange={() => setEncho((prev) => !prev)}
/>{" "}
Encho
</label> */}
</div>
</div>
<div className={styles.toolbox}>
{!isNew && (
<Stack direction="row" spacing={2}>
<Button
onClick={() => setEditMode((prev) => !prev)}
variant="contained"
startIcon={<EditIcon />}
size="small"
>
CANCEL
</Button>
<Button
onClick={handleSubmitUpdate}
variant="contained"
startIcon={<EditIcon />}
size="small"
>
Submit UPDATE
</Button>
</Stack>
)}
{isNew && (
<Stack direction="row" spacing={2}>
<Button
color="warning"
variant="contained"
startIcon={<CancelOutlinedIcon />}
size="small"
onClick={resetMatchData}
>
Reset
</Button>
<Button
onClick={handleSubmitNew}
variant="contained"
startIcon={<SendIcon />}
size="small"
>
Submit NEW
</Button>
</Stack>
)}
</div>
</div>
)
}
/edit 2: Solution
Based on Milos Pavlovic comment (marked solution), I've updated SingleMatchView.js. I put the matchData into it's own state and included a useEffect to listen to changes in matchData. Thanks!
Updated SingleMatchView.js:
// ...imports
export default function SingleMatchView({
tournamentId,
matchData,
isLoggedIn,
updateMatchInState,
deleteMatchInState
}) {
const hansokuIcon = "▲"
const [editMode, setEditMode] = useState(false)
const [confirmDelete, setConfirmDelete] = useState(false)
// deleted all individual state
// edit: even simpler, no state needed
// deleted: const [match, setMatch] = useState(matchData)
// added:
const match = matchData
useEffect(() => {
setMatch(matchData)
}, [matchData])
const redPoints =
match.red.ippon.length + Math.floor(match.white.hansoku / 2)
const whitePoints =
match.white.ippon.length + Math.floor(match.red.hansoku / 2)
...character limit
After looking at your code I would say that your problem lies inside SingleMatchView. As I can see you have multiple useStates that use matchData in order to fill state initial values. What is wrong here is the fact that you are not "resetting" those state values once you successfully update certain match.
SingleMatchView element will just rerender, and not to recreate, after update finish its work, meaning that you must find a way to reset(to new values) all those states that are using matchData prop for their values, otherwise you will end up with unchanged values across whole element lifecycle.
Let's explain problem using this line inside SingleMatchView:
const [redName, setRedName] = useState(matchData?.red?.name || "")
As we can see redName is initialized only once. Now imagine that inside SingleMatchEdit you are updating redName. What current code does is that inside handleSubmitUpdate you just call state updater from root component and that is all, problem is that SingleMatchView is not aware of update because this element uses its inner state, which is declared and assigned only once, and with that approach you decoupled from root component state and not listening to state updates at all - and that is why you will never rerender with updated info, because you never recompute your inner state inside SingleMatchView based on new prop value for matchData.
One solution would be to put useEffect inside SingleMatchView to listen for matchData change, and whenever prop value changes you should (re)set all states to new, latest, values, thus forcing content to rerender. Other solution is to intercept handler execution inside SingleMatchView, and before/after calling updateMatchInState you just (re)set state.
Been struggling to try and solve this one,
I have a <form> that is wrapped around a Controller using a render. The render is a list of <Chip>'s. I want the list of chips to act as a field, each user toggled chip would act as the data inputted to the form, based on the label value of a chip. The expected data can be either one string or an array of strings based on how many <Chips> are toggled.
I've tried to replicate examples that use <Checkbox>, but I wasn't able to re-create the component.
I have a state of total chips toggled, which re-produces the data I would like to submit via my button. Is there a way to pass this state to my form data?
I'm currently only able to submit empty data through my form because my chip values and form data are not linked up correctly.
To solve this I need to add onChange to my onClick parameter for <Chip>, I've tried doing this:
onClick={(value, e) => { handleToggle(value); onChange(value) }}
But that does not work?
Here is my Form
<form noValidate onSubmit = { handleSubmit(onSubmit) }>
<Controller
render={({ onChange ,...props }) => (
<Paper component="ul" className={classes.root}>
{stocklist.map((value, index) =>
{
return (
<li key={index}>
<Chip
name="stock_list"
variant="outlined"
label={value}
key={index}
color={checked.includes(value) ? 'secondary' : 'primary'}
onClick={handleToggle(value)}
className={classes.chip}
ref={register}
/>
</li>
);
})}
</Paper>
)}
name="stock_list"
control={control}
defaultValue={[]}
onChange={([, data]) => data}
/>
{checked && checked.length > 0 &&
<Fab
color="primary"
aria-label="delete"
type="submit"
>
<DeleteIcon />
</Fab>
}
</form>
Here is how I toggle the chips, and create a state checked which holds the values for every toggled chip
const { register, control, handleSubmit } = useForm();
const [checked, setChecked] = React.useState([]);
const handleToggle = (value) => () => {
const currentIndex = checked.indexOf(value);
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
}
setChecked(newChecked);
};
Here is my onSubmit function
const onSubmit = (data, e) =>
{
console.log(data);
axiosInstance
.patch('url', {
stock_list: data.stock_list,
})
.then((res) =>
{
console.log(res);
console.log(res.data);
});
};
Check it out https://codesandbox.io/s/cocky-roentgen-j0pcg Please toggle one of the chips, and then press the button that appears. If you then check the console, you will notice an empty form array being submitted.
I guess this will work for you
import React, { useState } from "react";
import { Chip, Paper, Fab, Grid } from "#material-ui/core/";
import { makeStyles } from "#material-ui/core/styles";
import { Controller, useForm } from "react-hook-form";
import DeleteIcon from "#material-ui/icons/Delete";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
justifyContent: "center",
flexWrap: "wrap",
listStyle: "none",
padding: theme.spacing(0.5),
margin: 0,
"#media (max-width: 600px)": {
overflowY: "auto",
height: 200
}
},
chip: {
margin: theme.spacing(0.5)
}
}));
export default function app() {
const { register, control, handleSubmit } = useForm();
const classes = useStyles();
const [checked, setChecked] = React.useState([]);
const stocklist = ["AAPL", "AMD", "TSLA"];
const onSubmit = (data, e) => {
console.log("data.stock_list");
console.log(data.stock_list);
console.log("data.stock_list");
};
const handleToggle = (value) => {// <== I changed this part
const currentIndex = checked.indexOf(value);
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
}
setChecked(newChecked);
return newChecked;// <== I changed this part
};
return (
<>
{(!stocklist || stocklist.length === 0) && (
<p style={{ textAlign: "center" }}>Your Bucket is empty...</p>
)}
{stocklist && stocklist.length > 0 && (
<Grid>
<form noValidate onSubmit={handleSubmit(onSubmit)}>
<Controller
name="stock_list"// <== I changed this part
render={({ onChange, ...props }) => (
<Paper component="ul" className={classes.root}>
{stocklist.map((value, index) => {
return (
<li key={index}>
<Chip
// name="stock_list"// <== I changed this part
variant="outlined"
label={value}
key={index}
color={
checked.includes(value) ? "secondary" : "primary"
}
onClick={(e) => {
onChange(handleToggle(value));// <== I changed this part
}}
className={classes.chip}
ref={register}
/>
</li>
);
})}
</Paper>
)}
control={control}
defaultValue={{}}
onChange={([, data]) => data}
/>
{checked && checked.length > 0 && (
<Fab color="primary" aria-label="delete" type="submit">
<DeleteIcon />
</Fab>
)}
</form>
</Grid>
)}
</>
);
}
On my UserProductsScreen.js file, I passed a parameter id so that I can pick it up on my navigation:
const UserProductsScreen = props => {
const userProducts = useSelector(state => state.products.userProducts);
// Navigate to EditProduct
const editProductHandler = (id) => {
props.navigation.navigate('EditProduct', { productId: id })
};
And then, inside my navigator.js file, I tried to check if it exist so that I can decide which header title to show either "Add Product" or "Edit Product":
<AdminNavigator.Screen
name="EditProduct"
component={EditProductScreen}
options={({ route }) => {
const productId = route.params.productId;
return {
title: productId ? "Edit Product" : "Add Product"
};
}}
/>
I tested this if it receive the productId and it did but then, I received this error when click on the plus icon: TypeError: undefined is not an object (evaluating 'route.params.productId')
I am not sure what it means but I am simply checking if it exist so that I can change the header dynamically.
For the record here's my complete navigation:
import React from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { createDrawerNavigator } from '#react-navigation/drawer';
import { Platform } from 'react-native';
import { Ionicons } from '#expo/vector-icons';
import ProductsOverviewScreen from '../screens/shop/ProductsOverviewScreen';
import ProductDetailScreen from '../screens/shop/ProductDetailScreen';
import CartScreen from '../screens/shop/CartScreen';
import OrdersScreen from '../screens/shop/OrdersScreen';
import UserProductsScreen from '../screens/user/UserProductsScreen';
import EditProductScreen from '../screens/user/EditProductScreen';
import Colors from '../constants/Colors';
import { HeaderButtons, Item } from 'react-navigation-header-buttons';
import HeaderButton from '../components/UI/HeaderButton';
const ProductsNavigator = createStackNavigator();
const ProductsNavMenu = () => {
return (
<ProductsNavigator.Navigator
screenOptions={{
headerStyle: {
backgroundColor: Platform.OS === 'android' ? Colors.primary : ''
},
headerTintColor: Platform.OS === 'android' ? 'white' : Colors.primary,
headerTitleStyle: {
fontSize: 17,
fontFamily: 'poppins-bold'
},
headerBackTitleStyle: {
fontFamily: 'poppins-regular'
}
}}>
<ProductsNavigator.Screen
name="Products"
component={ProductsOverviewScreen}
options={({ navigation }) => {
return {
headerRight: () => (
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title="Cart"
iconName={Platform.OS === 'android' ? 'md-cart' : 'ios-cart'}
onPress={() => navigation.navigate('Cart')}
/>
</HeaderButtons>
),
headerLeft: () => (
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title='Menu'
iconName={Platform.OS === 'android' ? 'md-menu' : 'ios-menu'}
onPress={() => {
navigation.toggleDrawer();
}}
/>
</HeaderButtons>
)
};
}}
/>
<ProductsNavigator.Screen
name="ProductDetail"
component={ProductDetailScreen}
options={({ route }) => {
const productTitle = route.params.productTitle;
return {
title: productTitle
};
}}
/>
<ProductsNavigator.Screen
name="Cart"
component={CartScreen}
/>
</ProductsNavigator.Navigator>
);
};
// Create a separate stack navigation
// for orders
const OrdersNavigator = createStackNavigator();
const OrdersNavMenu = () => {
return (
<OrdersNavigator.Navigator
mode="modal"
screenOptions={{
headerStyle: {
backgroundColor: Platform.OS === 'android' ? Colors.primary : ''
},
headerTintColor: Platform.OS === 'android' ? 'white' : Colors.primary,
headerTitleStyle: {
fontSize: 17,
fontFamily: 'poppins-bold'
},
headerBackTitleStyle: {
fontFamily: 'poppins-regular'
}
}}
>
<OrdersNavigator.Screen
name="Orders"
component={OrdersScreen}
options={({ navigation }) => {
return {
headerLeft: () => (
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title='Menu'
iconName={Platform.OS === 'android' ? 'md-menu' : 'ios-menu'}
onPress={() => {
navigation.toggleDrawer();
}}
/>
</HeaderButtons>
)
};
}}
/>
</OrdersNavigator.Navigator>
);
};
// Create a separate stack navigation
// for userProductsScreen
const AdminNavigator = createStackNavigator();
const AdminNavMenu = () => {
return (
<AdminNavigator.Navigator
mode="modal"
screenOptions={{
headerStyle: {
backgroundColor: Platform.OS === 'android' ? Colors.primary : ''
},
headerTintColor: Platform.OS === 'android' ? 'white' : Colors.primary,
headerTitleStyle: {
fontSize: 17,
fontFamily: 'poppins-bold'
},
headerBackTitleStyle: {
fontFamily: 'poppins-regular'
}
}}
>
<AdminNavigator.Screen
name="UserProducts"
component={UserProductsScreen}
options={({ navigation }) => {
return {
title: "User Products",
headerLeft: () => (
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title='User Products'
iconName={Platform.OS === 'android' ? 'md-list' : 'ios-list'}
onPress={() => {
navigation.toggleDrawer();
}}
/>
</HeaderButtons>
),
headerRight: () => (
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title='Add'
iconName={Platform.OS === 'android' ? 'md-create' : 'ios-create'}
onPress={() => {
navigation.navigate('EditProduct');
}}
/>
</HeaderButtons>
)
};
}}
/>
<AdminNavigator.Screen
name="EditProduct"
component={EditProductScreen}
options={({ route }) => {
const productId = route.params.productId;
return {
title: productId ? "Edit Product" : "Add Product"
};
}}
/>
</AdminNavigator.Navigator>
);
};
const ShopNavigator = createDrawerNavigator();
const ShopNavMenu = () => {
return (
<NavigationContainer>
<ShopNavigator.Navigator>
<ShopNavigator.Screen
name="Products"
component={ProductsNavMenu}
options={{
drawerIcon: ({ focused, size }) => (
<Ionicons
name={Platform.OS === 'android' ? 'md-cart' : 'ios-cart'}
size={23}
color={focused ? '#7cc' : '#ccc'}
/>
)
}}
/>
<ShopNavigator.Screen
name="Orders"
component={OrdersNavMenu}
options={{
drawerIcon: ({ focused, size }) => (
<Ionicons
name={Platform.OS === 'android' ? 'md-list' : 'ios-list'}
size={23}
color={focused ? '#7cc' : '#ccc'}
/>
)
}}
/>
<ShopNavigator.Screen
name="Admin"
component={AdminNavMenu}
options={{
drawerIcon: ({ focused, size }) => (
<Ionicons
name={Platform.OS === 'android' ? 'md-create' : 'ios-create'}
size={23}
color={focused ? '#7cc' : '#ccc'}
/>
)
}}
/>
</ShopNavigator.Navigator>
</NavigationContainer>
);
};
export default ShopNavMenu;
Upate: So I tried to use setParams on UserProductsScreen where the magic is happening. I use useEffect to set an initial productId to blank:
import React, { useEffect } from 'react';
import { CommonActions } from '#react-navigation/native';
const UserProductsScreen = props => {
const userProducts = useSelector(state => state.products.userProducts);
// Navigate to EditProduct
const editProductHandler = (id) => {
props.navigation.navigate('EditProduct', { productId: id })
};
// Set initial productId params not until the editProductHandler is triggerred
useEffect(() => {
props.navigation.dispatch(CommonActions.setParams({ productId: '' }));
}, []);
// dispatch
const dispatch = useDispatch();
And then on my navigation I tried to catch the the productId via my headerRight:
<AdminNavigator.Screen
name="UserProducts"
component={UserProductsScreen}
options={({ navigation, route }) => {
const productId = route.params.productId;
return {
title: "User Products",
headerLeft: () => (
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title='User Products'
iconName={Platform.OS === 'android' ? 'md-list' : 'ios-list'}
onPress={() => {
navigation.toggleDrawer();
}}
/>
</HeaderButtons>
),
headerRight: () => (
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title='Add'
iconName={Platform.OS === 'android' ? 'md-create' : 'ios-create'}
onPress={() => {
navigation.navigate('EditProduct');
}}
/>
</HeaderButtons>
)
};
}}
/>
But this affects the id also on the regular navigation. Now the error is above is also showing when clicking the edit button.
The problem is here
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title='Add'
iconName={Platform.OS === 'android' ? 'md-create' : 'ios-create'}
onPress={() => {
navigation.navigate('EditProduct');
}}
/>
</HeaderButtons>
When you are navigating from the header right button you are not passing any parameters.
If you do not have access to route params at this point, you will have to use the setParams to set the parameters after you navigate to the page.
You can refer the doc
https://reactnavigation.org/docs/navigation-prop#setparams
Update :
so when you navigate wihout passing anything the route.params is undefined
when you access a property if undefined it throws an error
The way to validate this is to use the Optional Chaining like below
const productId = route.params?.productId;
This will set the value only if params is available.
I had this issue when my first screen loads. The params were undefined and I fixed it by using the initialParams option to set the params for my first screen.
<Stack.Screen
component={LCWebView}
options={{
title: 'Awesome app',
myopt: 'test',
}}
initialParams={{ url: url_goes_here }}
/>
i'm trying to create a custom color picker with formik form
the probleme her is that parent component color are not changed :
import {SketchPicker} from "react-color";
export const MyColorPicker = ({label, ...props}) => {
// with useField is should not use onChange but i get an error without defining it myself
const [field] = useField(props);
const [color, setColor] = useState("#333");
const handleChange = color => {
setColor(color.hex);
field.onChange(color.hex);
};
const onComplete = color => {
setColor(color.hex);
field.onChange(color.hex);
};
return (
<div style={{padding: 10}}>
<label>{label}</label>
<SketchPicker {...props} {...field} color={color} onChange={handleChange} onChangeComplete={onComplete} />
</div>
);
};
as exemple this work :
export const MyTextAreaField = ({label, ...props}) => {
const [field, meta] = useField(props);
if (field && field.value === null) {
field.value = "";
}
return (
<div style={{display: "flex", flexDirection: "column"}}>
<label className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated MuiInputLabel-shrink MuiFormLabel-filled">
{label}
</label>
<TextareaAutosize
rows={10}
{...field}
{...props}
style={{marginTop: 10, fontFamily: "Helvetica Neue", fontSize: 15}}
/>
{meta.touched && meta.error ? <div className="error">{meta.error}</div> : null}
</div>
);
};
and parent code :
<Formik
initialValues={{
data: { title :'', shortDescription:'', description:'', color:'')
}}
onSubmit={values => {
console.log(values.data) ; // data.color stay null
}}>
<Form>
<MyTextAreaField id="data.description" name="data.description" label={t("PROJECT.DESCRIPTION")} />
<MyColorPicker id="data.color" label={t("PROJET.COLOR")} name="data.color" />
</Form>
</Formik>
finally i ended doing something like this :
In parent Component :
<MyColorPicker
label={t("PROJECT.COLOR")}
onChange={color => {
data.project.color = color;
}}
/>
Component definition
export const MyColorPicker = ({label, onChange}) => {
const [color, setColor] = useState("#333");
const handleChange = color => {
setColor(color.hex);
};
return (
<div
style={{display: "flex", flexDirection: "row", justifyContent: "flex-start", alignItems: "center", padding: 10}}>
<label>{label}</label>
<ChromePicker color={color} onChange={handleChange} onChangeComplete={color=> onChange(color.hex) } />
</div>
)
})
//Main functional component
function CustomizedTables(props) {
const classes = useStyles();
const [nameCheckbox, setnameCheckbox] = useState([]);
const [emailCheckbox, setemailCheckbox] = useState([]);
const [faxCheckbox, setfaxCheckbox] = useState([]);
const [loopStopFlag, setFlag] = useState(true);
const [emailFlag, setEmailFlag] = useState(false);
const rows = props.data;
//Initialization of checkboxes to true
if (loopStopFlag) {
let nameArray = [];
let emailArray = [];
let faxArray = [];
rows.SupplierList.forEach((supplier, i) => {
nameArray.push(true);
if (supplier.Email) {
emailArray.push(true);
} else {
emailArray.push(null);
}
if (supplier.Fax) {
faxArray.push(true);
} else {
faxArray.push(null);
}
});
setnameCheckbox(nameArray);
setemailCheckbox(emailArray);
setfaxCheckbox(faxArray);
setFlag(false);
}
//Reseting all checkboxes
const resetAllCheckbox = () => {
let nameArray = [];
let emailArray = [];
let faxArray = [];
rows.SupplierList.forEach((supplier, i) => {
nameArray.push(false);
if (supplier.Email) {
emailArray.push(false);
} else {
emailArray.push(null);
}
if (supplier.Fax) {
faxArray.push(false);
} else {
faxArray.push(null);
}
});
setnameCheckbox(nameArray);
setemailCheckbox(emailArray);
setfaxCheckbox(faxArray);
if (emailCheckbox.includes(true)) {
setEmailFlag(false);
} else {
setEmailFlag(true);
}
};
//Displaying data on screen
return (
<div>
<Paper className={classes.root} style={{ marginLeft: "32px" }}>
<Table className={classes.table} aria-label="customized table">
<TableBody>
{rows.SupplierList.map((row, i) => (
<StyledTableRow key={i}>
<StyledTableCell style={{ color: "#474747" }} align="center">
<label>
{row.Name}{" "}
<input
type="checkbox"
style={{ marginLeft: "5px", cursor: "pointer" }}
onChange={() => {
let arr = nameCheckbox;
arr[i] = !arr[i];
setnameCheckbox(arr);
}}
defaultChecked={nameCheckbox[i]}
/>
</label>
<br />
<div style={{ fontSize: "11px" }}>{row.Gbf && "GBF"}</div>
</StyledTableCell>
<StyledTableCell align="center" style={{ color: "#474747" }}>
{row.Email && (
<React.Fragment>
<input
type="checkbox"
style={{ marginLeft: "5px", cursor: "pointer" }}
onChange={() => {
let arr1 = emailCheckbox;
arr1[i] = !arr1[i];
setemailCheckbox(arr1);
if (emailCheckbox.includes(true)) {
setEmailFlag(false);
} else {
setEmailFlag(true);
}
}}
defaultChecked={emailCheckbox[i]}
/>
</React.Fragment>
)}
</StyledTableCell>
<StyledTableCell align="center" style={{ color: "#474747" }}>
{row.Fax && (
<React.Fragment>
<input
type="checkbox"
style={{ marginLeft: "5px", cursor: "pointer" }}
onChange={() => {
let arr2 = faxCheckbox;
arr2[i] = !arr2[i];
setfaxCheckbox(arr2);
}}
value={faxCheckbox[i]}
defaultChecked={faxCheckbox[i]}
/>
</React.Fragment>
)}
</StyledTableCell>
</StyledTableRow>
))}
</TableBody>
</Table>
</Paper>
<br />
<Button
variant="danger"
style={{ width: "100px" }}
onClick={resetAllCheckbox}
>
Reset
</Button>
</div>
);
}
export default CustomizedTables;
I am facing problem in reseting checkboxes. When I reset them only array is updating but no action is performing or checkboxes. I used both checked and defaultChecked in checked also facing problem that bool is updating but checkbox is not updating. I want a solution for it. Actually when I click on reset button then my checkboxes remain same form but state is updating
I couldn't replicate your exact code in a sandbox because of it's dependencies so I've put up a simple example that has a set of checkboxes, check action and reset action.
Modify your code based on the example and these notes and it would work as expected:
Whenever you render a react element from inside loops you should provide a unique key, this helps react identify what changed.
Keep your checkbox state in your react/store state and set it using checked prop and not defaultChecked. On reset update the store/local states and the component will re-render.
Check the sandbox:
https://codesandbox.io/s/pedantic-violet-twtvn?fontsize=14&hidenavigation=1&theme=dark
import React, { useState, useEffect } from "react";
function CheckboxDemo({ data }) {
const [features, setFeatures] = useState([]);
useEffect(() => {
setFeatures(data);
}, [data]);
const resetCheckboxes = () => {
const newFeatures = [];
features.forEach(f => {
newFeatures.push({ ...f, checked: false });
});
setFeatures(newFeatures);
};
const setFeatureCheck = index => () => {
const updatedFeature = {
...features[index],
checked: !features[index].checked
};
const newFeatures = [
...features.slice(0, index),
updatedFeature,
...features.slice(index + 1)
];
setFeatures(newFeatures);
};
const checkBoxes = features.map((f, i) => (
<div className="box" key={i + "_" + f.label}>
<input
type="checkbox"
checked={f.checked}
onChange={setFeatureCheck(i)}
/>{" "}
{f.label}
</div>
));
return (
<div className="wrapper">
{checkBoxes}
<div className="controls">
<input type="button" onClick={resetCheckboxes} value="Reset" />
</div>
</div>
);
}
export default CheckboxDemo;