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.
I am writing an ecommerce webshop using React js and Commerce.js
I am very confused as I am not able to identify the precise problem. But here's how it's happening:
My App.js:
const App = () => {
const [products, setProducts] = useState([]);
const [cart, setCart] = useState([]);
const fetchProducts = async () => {
const { data } = await commerce.products.list();
setProducts(data);
};
const fetchCart = async () => {
setCart(await commerce.cart.retrieve());
};
const handleAddToCart = async (productId, quantity) => {
const item = await commerce.cart.add(productId, quantity);
setCart(item.cart);
};
useEffect(() => {
fetchProducts();
fetchCart();
}, []);
console.log(cart);
return (
<>
<Navbar totalItems={cart.total_items} />
{/* <Products products={products} onAddToCart={handleAddToCart} /> */}
{/* <Cart cartItems={cart} /> */}
</>
);
Now when I uncomment the <Cart cartItems={cart} />, React js Throws an error
This is the error in from the console
Uncaught Error: Objects are not valid as a React child (found: object with keys {raw, formatted, formatted_with_symbol, formatted_with_code}). If you meant to render a collection of children, use an array instead.
Interestingly enough, the single item is being passed on through the Cart.js but not without the error.
Here's Cart.js for Reference
const Cart = ({ cartItems }) => {
const classes = useStyles();
const EmptyCart = () => {
return (
<Typography variant="subtitle1">
You have no items in your cart. Start adding some :)
</Typography>
);
};
const FilledCart = () => {
return (
<>
<Grid container spacing={3}>
{cartItems.line_items.map((item) => (
<Grid item xs={12} sm={4} key={item.id}>
<CartItem items={item} />
</Grid>
))}
</Grid>
<div className={classes.cardDetails}>
<Typography variant="h4">
Subtotal: {cartItems.subtotal.formatted_with_symbol}
</Typography>
<div>
<Button
className={classes.emptyButton}
size="large"
type="button"
variant="contained"
color="secondary"
>
Empty Cart
</Button>
<Button
className={classes.checkoutButton}
size="large"
type="button"
variant="contained"
color="primary"
>
Checkout
</Button>
</div>
</div>
</>
);
};
if (!cartItems.line_items)
return <Typography variant="h4">Loading...</Typography>;
return (
<Container>
<div className={classes.toolbar} />
<Typography className={classes.title} variant="h3">
Your Shopping Cart
</Typography>
{!cartItems.line_items.length ? <EmptyCart /> : <FilledCart />}
</Container>
);
};
Update:
Here's what Cart object looks like
In React-Admin when i move the child components to another component and try to render it inside the simpleform tag then textfields are not showing its value and textinput tag CSS also goes missing. what i am trying to do is to create a common component for the create and edit tag so i break it down it to multiple components and then try to render it using props.children
export const AssessmentEdit = props => {
return (
<CommonComponents {...props} componentType='edit' notif='Assessment Preference Updated successfully' redirect='list' validation={validateAssessment}>
<FormData {...props} componentType='edit'/>
</CommonComponents>
)
};
const CommonComponent = (props) => {
const notify = useNotify();
const redirect = useRedirect();
const onSuccess = () => {
notify(props.notif);
redirect(props.redirect, props.basePath);
};
const Compo = components[props.componentType];
console.log(props.componentType)
return (
<Compo {...props}
undoable={false}
onSuccess={onSuccess}>
{props.componentType === 'show' ? <SimpleShowLayout>
{props.children}
</SimpleShowLayout> : <SimpleForm
redirect={props.redirect}
validate={props.validation}>
{props.children}
</SimpleForm>}
</Compo>
);
};
export const FormData = (props) => {
const classes = utilStyles();
return (
<React.Fragment>
{props.componentType === 'edit' && <>
<TextField {...props} source="id" label="Id"/>
<TextField {...props} source="organization_id" label="Organization"/>
<TextField {...props} source="provider" label="Provider"/>
</>}
<TextInput source="name" label={'Name *'}/>
<SelectInput source="category"
label={'Category *'}
choices={AssessmentCategory}
optionText="name"
optionValue="value"/>
<ArrayInput source="topics">
<SimpleFormIterator>
<TextInput/>
</SimpleFormIterator>
</ArrayInput>
<TextInput source="description"
label={'Description *'}
className={classes.fullWidth}
options={{multiLine: true}}/>
<RichTextInput source="instructions"
label={'Instructions *'}/>
<NumberInput source="duration"
label={'Duration *'}/>
<BooleanInput source="randomize_questions"/>
<FormDataConsumer>
{({formData, formData: {randomize_questions}}) => {
if (randomize_questions) {
return <NumberInput source="question_count" label={'Question Count *'}/>
}
return null;
}}
</FormDataConsumer>
<ArrayInput source="questions"
label={'Questions *'}>
<SimpleFormIterator>
<ReferenceInput source="questionId"
className={classes.fullWidth}
label={"Question"}
reference="search-questions">
<AutocompleteInput optionValue="id"
matchSuggestion={() => true}
inputText={(value) => {
return value && value.question_text && value.question_text.slice(0, 200)
}}
className={classes.fullWidth}
optionText={<Custom/>}/>
</ReferenceInput>
<NumberInput label="Question Weight" source="question_weight"/>
</SimpleFormIterator>
</ArrayInput>
<ArrayInput source="skills" label={'Skills *'}>
<SimpleFormIterator>
<ReferenceInput source="skillId"
label={"Skill"}
className={classes.fullWidth}
reference="perform-skill-search">
<AutocompleteInput optionValue="id"
className={classes.fullWidth}
optionText="display_name"/>
</ReferenceInput>
<SelectInput label="Skill Level"
choices={levels}
optionText="key"
optionValue="value"
source="skill_level"/>
</SimpleFormIterator>
</ArrayInput>
</React.Fragment>
);
};
I ended up creating my own TextField component and explicitely passing down the props:
interface CustomTextFieldProps {
label?: string,
record?: Record,
source: string
}
const CustomTextField = (props: CustomTextFieldProps) => (
<Labeled label={props.label ? props.label : startCase(props.source)}>
<span>{get(props.record, props.source)}</span>
</Labeled>
);
Usage:
<CustomTextField source="fieldName" record={props.record} />
Before diving to the main problem, my use case is I am trying to handle the scroll to a desired section. I will have navigations on the left and list of form sections relative to those navigation on the right. The Navigation and Form Section are the child component. Here is how I have structured my code
Parent.js
const scrollToRef = ref => window.scrollTo(0, ref.current.offsetTop);
const Profile = () => {
const socialRef = React.useRef(null);
const smsRef = React.useRef(null);
const handleScroll = ref => {
console.log("scrollRef", ref);
scrollToRef(ref);
};
return (
<>
<Wrapper>
<Grid>
<Row>
<Col xs={12} md={3} sm={12}>
<Navigation
socialRef={socialRef}
smsRef={smsRef}
handleScroll={handleScroll}
/>
</Col>
<Col xs={12} md={9} sm={12}>
<Form
socialRef={socialRef}
smsRef={smsRef}
/>
</Col>
</Row>
</Grid>
</Wrapper>
</>
);
};
Navigation.js(child component)
I tried using forwardRef but seems like it only accepts one argument as ref though I have multiple refs.
const Navigation = React.forwardRef(({ handleScroll }, ref) => {
// it only accepts on ref argument
const items = [
{ id: 1, name: "Social connections", pointer: "social-connections", to: ref }, // socialRef
{ id: 2, name: "SMS preference", pointer: "sms", to: ref }, // smsRef
];
return (
<>
<Box>
<UL>
{items.map(item => {
return (
<LI
key={item.id}
active={item.active}
onClick={() => handleScroll(item.to)}
>
{item.name}
</LI>
);
})}
</UL>
</Box>
</>
);
});
export default Navigation;
Form.js
I do not have idea on passing multiple refs when using forwardRef so for form section I have passed the refs as simple props passing.
const Form = ({ socialRef, smsRef }) => {
return (
<>
<Formik initialValues={initialValues()}>
{({ handleSubmit }) => {
return (
<form onSubmit={handleSubmit}>
<Social socialRef={socialRef} />
<SMS smsRef={smsRef} />
</form>
);
}}
</Formik>
</>
);
};
Social.js
const Social = ({ socialRef }) => {
return (
<>
<Row ref={socialRef}>
<Col xs={12} md={3}>
<Label>Social connections</Label>
</Col>
<Col xs={12} md={6}></Col>
</Row>
</>
);
};
Can anyone help me at passing multiple refs so when clicked on the particular navigation item, it should scroll me to its respective component(section).
I have added an example below. I have not tested this. This is just the idea.
import React, { createContext, useState, useContext, useRef, useEffect } from 'react'
export const RefContext = createContext({});
export const RefContextProvider = ({ children }) => {
const [refs, setRefs] = useState({});
return <RefContext.Provider value={{ refs, setRefs }}>
{children}
</RefContext.Provider>;
};
const Profile = ({ children }) => {
// ---------------- Here you can access refs set in the Navigation
const { refs } = useContext(RefContext);
console.log(refs.socialRef, refs.smsRef);
return <>
{children}
</>;
};
const Navigation = () => {
const socialRef = useRef(null);
const smsRef = useRef(null);
const { setRefs } = useContext(RefContext);
// --------------- Here you add the refs to context
useEffect(() => {
if (socialRef && smsRef) {
setRefs({ socialRef, smsRef });
}
}, [socialRef, smsRef, setRefs]);
return <>
<div ref={socialRef}></div>
<div ref={smsRef}></div>
</>
};
export const Example = () => {
return (
<RefContextProvider>
<Profile>
<Navigation />
</Profile>
</RefContextProvider>
);
};
I am opening Model (child Component) on Button Click from Parent Component, it opens very well but its not closing and it shows some error:
Uncaught TypeError: setOpen is not a function from Child Component
Here is My Parent Component
<TableCell>
<Button
variant="contained"
size="small"
color="primary"
onClick={() => deleteHandler(index)}
>
Delete Me
</Button>
</TableCell>
{console.log(open)}
{open && <AddList open={open} setOpen={open} />}
My Child Component
export default function TransitionsModal(open, setOpen) {
const classes = useStyles();
// const [openL, setOpenL] = React.useState(null);
// const handleOpen = () => {
// setOpen(true);
// };
const handleClose = () => {
setOpen(!open);
};
return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
className={classes.modal}
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500
}}
>
<Fade in={open}>
<div className={classes.paper}>
<h2 id="transition-modal-title">Transition modal</h2>
<p id="transition-modal-description">
react-transition-group animates me.
</p>
</div>
</Fade>
</Modal>
</div>
);
}
Your first issue is that you are passing a Boolean for the setOpen prop rather than the setOpen function itself, so change it to setOpen={setOpen}.
// RenderList.js
const RenderList = props => {
// ...
return (
...
{open && <AddList open={open} setOpen={setOpen} />}
)
}
Your second issue is that you're not destructing props properly in the TransitionsModal component. Use {} to destruct the props object and grab what you need.
// AddList.js
export default function TransitionsModal({ open, setOpen }) {
// ...
}
Here's the fixed example:
CodeSandbox
Hope this helps.
Hi take a look at this
https://codesandbox.io/s/frosty-bird-5yh5g
in RenderList.js you didn't pass setOpen
{open && <AddList open={open} setOpen={setOpen} />}
also export default function TransitionsModal({ open, setOpen }) {