Reputation: 1274
I have a parent component with a couple children components inside of it. I want to be able to set the status of the parent depending on the state of the children.
For example, imagine a parent component with 3 children who all get their status from a server. They then set the status of the parent so that the parent can see if all children have issues, or just a couple for example.
Parent:
const Parent = () => {
const [hasIssues, setHasIssues] = useState({
"child-item-1": false,
"child-item-2": false,
"child-item-3": false
});
const issuesHandler = (childName, childStatus) => {
setHasIssues({ ...hasIssues, [childName]: childStatus });
};
return (
<div>
<pre>{JSON.stringify(hasIssues, null, 2)}</pre>
<div>
<ChildItemOne issuesHandler={issuesHandler} />
<ChildItemTwo issuesHandler={issuesHandler} />
<ChildItemThree issuesHandler={issuesHandler} />
</div>
</div>
);
};
A child example:
const ChildItemOne = ({ issuesHandler }) => {
// Imagine this is actually retrieved from a server
const hasIssues = Math.random() <= 0.75;
issuesHandler("child-item-1", hasIssues);
return <div>{`child-item-1: ${hasIssues}`}</div>;
};
Of course this example will exceed the maximum update depth. I have tried preventing this by using the following for example:
useEffect(() => {
issuesHandler("child-item-1", hasIssues);
}, [hasIssues, issuesHandler]);
but this still does not have the intended result as the state is still constantly updated, but without it exceeding the maximum update depth: Codesandbox example.
I have also tried using useCallback
, which didn't do anything. Changing the dependencies of useEffect
to an empty array (ESLint doesn't let me do that locally anyway), didn't work either:
What's the best way to set a parent's state from a child component without it causing constant re-rendering?
Upvotes: 1
Views: 327
Reputation: 191946
Move const hasIssues = Math.random() <= 0.75;
into the useEffect()
callback. This will prevent the rerender loop, since the value will only be generated once.
This will also simulate the call to the server better, since it will only happen once, when the component is mounted.
const ChildItemOne = ({ issuesHandler, hasIssues }) => {
useEffect(() => {
// Imagine this is actually retrieved from a server
issuesHandler("child-item-1", Math.random() <= 0.75);
}, [issuesHandler]);
return <div>{`child-item-1: ${hasIssues}`}</div>;
};
The parent should also pass hasIssues
back to the children wrap issuesHandler
with useCallback()
, and use the updater callback in setHasIssues()
. This will prevent the issuesHandler
from being recreated on each render, which will in turn cause the children's useEffect()
to be called, and so on...
const Parent = () => {
const [hasIssues, setHasIssues] = useState({
"child-item-1": false,
"child-item-2": false,
"child-item-3": false
});
const issuesHandler = useCallback((childName, childStatus) => {
setHasIssues(state => ({ ...state, [childName]: childStatus }))
}, [setHasIssues]);
return (
<div>
<pre>{JSON.stringify(hasIssues, null, 2)}</pre>
<div>
<ChildItemOne issuesHandler={issuesHandler} hasIssues={hasIssues['child-item-1']} />
<ChildItemTwo issuesHandler={issuesHandler} hasIssues={hasIssues['child-item-2']} />
<ChildItemThree issuesHandler={issuesHandler} hasIssues={hasIssues['child-item-3']} />
</div>
</div>
);
};
Live example:
const { useState, useCallback, useEffect } = React;
const ChildItemOne = ({ issuesHandler, hasIssues }) => {
useEffect(() => {
// Imagine this is actually retrieved from a server
issuesHandler("child-item-1", Math.random() <= 0.75);
}, [issuesHandler]);
return <div>{`child-item-1: ${hasIssues}`}</div>;
};
const Parent = () => {
const [hasIssues, setHasIssues] = useState({
"child-item-1": false,
"child-item-2": false,
"child-item-3": false
});
const issuesHandler = useCallback((childName, childStatus) => {
setHasIssues(state => ({ ...state, [childName]: childStatus }))
}, [setHasIssues]);
return (
<div>
<pre>{JSON.stringify(hasIssues, null, 2)}</pre>
<div>
<ChildItemOne issuesHandler={issuesHandler} hasIssues={hasIssues['child-item-1']} />
</div>
</div>
);
};
ReactDOM.render(
<Parent />,
root
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
Upvotes: 1
Reputation: 1821
This looks like a case for useContext. React Context is used to provide access to state/function for both parent and children components without the use of the callback hell.
First create a dummy context component somewhere in the app, then import it to the Parent component and use it to wrap around components that need the pass down. And then instead of passing your states/functions to the child, you just pass it to the context component.
In your child components, you can access those data by using useContext.
issueContext.js (dummy context file)
// app/context/issueContext.js
import { createContext } from 'react';
export default createContext();
parent component
import IssueContext from 'issueContext';
const Parent = () => {
const [hasIssues, setHasIssues] = useState({ ... });
const issuesHandler = (childName, childStatus) => { ... };
return (
<>
<IssueContext.Provider value={{ hasIssues, setHasIssues, issuesHandler }} >
<ChildItemOne />
<ChildItemTwo />
<ChildItemThree />
</IssueContext>
</>
);
};
Child component
import React, {useContext} from 'react';
import IssueContext from 'issueContext';
const ChildItemOne = () => {
const {hasIssues, setHasIssues, issuesHandler} = useContext(IssueContext);
if (something wrong) {
sethasIssues();
issuesHandler("child-item-1", hasIssues);
}
};
Upvotes: 1