I have a state variable which holds components created dynamically, however, when I access the state from a function passed to the child as props, I get the state status from back when it was created. Not so when I log useEffect.
For example: I add 3 children, and in the function logMyChildren I get the state previous to the creation of the last Child element.
First Child mychildren is []
Second Child myChildren is [{Child with id 0}]
Third Child myChildren is [{Child with id 0}, {Child with id 1}]
It gives me the same state with each Child every time I call that function.
Is there a way to get the current state(not a state from the past) regardless of the children?
const Parent = () => {
const [myChildren, setMyChildren] = useState([])
const addChild = () => {
let id = myChildren.length + 1
setMyChildren([
...myChildren,
<Child key={id} id={id} logMyChildren={logMyChildren} />,
])
}
const logMyChildren = (id) => {
console.log(id, myChildren)
}
useEffect(() => {
console.log(myChildren)
}, [myChildren])
return (
<>
<button onClick={addChild}>Add a child</button>
{myChildren && myChildren.map((child) => child)}
</>
)
}
const Child = ({ id, logMyChildren }) => {
return (
<>
<p>A child with id {id}!</p>
<button onClick={() => logMyChildren(id)}>X</button>
</>
)
}
Every time useEffect() runs, it has the updated state.
Thanks.
The problem for you is that you are creating logMyChildren that encloses state variable (in your case mychildren).
What you could do is to use useRef
Something like this:
const stateRef = useRef();
stateRef.current = myChildren;
And then in logMyChildren you use ref - stateRef:
console.log(id,stateRef.current);
Related
I have a parent component with a handler function:
const folderRef = useRef();
const handleCollapseAllFolders = () => {
folderRef.current.handleCloseAllFolders();
};
In the parent, I'm rendering multiple items (folders):
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
name={folder.name}
content={folder.content}
id={folder.id}
ref={folderRef}
/>
))}
In the child component I'm using the useImperativeHandle hook to be able to access the child function in the parent:
const [isFolderOpen, setIsFolderOpen] = useState(false);
// Collapse all
useImperativeHandle(ref, () => ({
handleCloseAllFolders: () => setIsFolderOpen(false),
}));
The problem is, when clicking the button in the parent, it only collapses the last opened folder and not all of them.
Clicking this:
<IconButton
onClick={handleCollapseAllFolders}
>
<UnfoldLessIcon />
</IconButton>
Only collapses the last opened folder.
When clicking the button, I want to set the state of ALL opened folders to false not just the last opened one.
Any way to solve this problem?
You could create a "multi-ref" - ref object that stores an array of every rendered Folder component. Then, just iterate over every element and call the closing function.
export default function App() {
const ref = useRef([]);
const content = data.map(({ id }, idx) => (
<Folder key={id} ref={(el) => (ref.current[idx] = el)} />
));
return (
<div className="App">
<button
onClick={() => {
ref.current.forEach((el) => el.handleClose());
}}
>
Close all
</button>
{content}
</div>
);
}
Codesandbox: https://codesandbox.io/s/magical-cray-9ylred?file=/src/App.js
For each map you generate new object, they do not seem to share state. Try using context
You are only updating the state in one child component. You need to lift up the state.
Additionally, using the useImperativeHandle hook is a bit unnecessary here. Instead, you can simply pass a handler function to the child component.
In the parent:
const [isAllOpen, setAllOpen] = useState(false);
return (
// ...
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
isOpen={isAllOpen}
toggleAll={setAllOpen(!isAllOpen)}
// ...
/>
))}
)
In the child component:
const Child = ({ isOpen, toggleAll }) => {
const [isFolderOpen, setIsFolderOpen] = useState(false);
useEffect(() => {
setIsFolderOpen(isOpen);
}, [isOpen]);
return (
// ...
<IconButton
onClick={toggleAll}
>
<UnfoldLessIcon />
</IconButton>
)
}
I've spent a few days on this and it is driving me crazy now.
I have a state in a parent component containing an Array[string] of selected squares which is passed to the child component (a map) along with the set function from the hook. The issue is that when I set the new squares they are changed in the parent, but on selection of another square it is not taking into account the already selected squares.
function Parent(props){
const [selectedSquares, setSquares] = useState([]);
useEffect(() => {
console.log('parent useEffect', selectedSquares);
}, [selectedSquares]);
return (
<Child selectedSquares={selectedSquares}
handleSquaresChange={setSquares}
/>
)
}
function Child(props){
const {selectedSquares, handleSquaresChange} = props;
useEffect(() => {
console.log('child useEffect', selectedSquares)
}, [selectedSquares]);
const handleSelect = evt => {
if(evt.target){
const features = evt.target.getFeatures().getArray();
let selectedFeature = features.length ? features[0] : null;
if (selectedFeature) {
console.log('select (preadd):', selectedSquares);
const newTile = selectedFeature.get('TILE_NAME');
const newSquares = [...selectedSquares];
newSquares.push(newTile);
const newTest = 'newTest';
handleSquaresChange(newSquares);
console.log('select (postadd):', newSquares);
}
}
return(
<Map>
<Select onSelect={handleSelect}/>
</Map>
)
}
On the first interactionSelect component I get this output from the console:
parent useEffect: [],
child useEffect: [],
select (preadd):[],
child useEffect:['NX'],
parent useEffect: ['NX'],
select (postadd): ['NX'].
Making the second selection this is added to the console:
select (preadd):[],
select (postadd): ['SZ'],
child useEffect:['SZ'],
parent useEffect: ['SZ'].
Turns out there is an addEventListener in the library I am using that is going wrong. Thanks to everyone who responded but turns out the issue was not with React or the state stuff.
Consider something like the code below. Your parent has an array with all your options. For each option, you render a child component. The child component handles the activity of its own state.
function Parent(props){
// array of options (currently an array of strings, but this can be your squares)
const allOptions = ['opt 1', 'opt 2', 'opt 3', 'etc'];
return (
<>
// map over the options and pass option to child component
{allOptions.map((option) => <Child option={option}/>)}
</>
)
}
function Child({ option }){
const [selected, setSelected] = useState(false); // default state is false
return (
<>
// render option value
<p>{option}</p>
// shows the state as selected or not selected
<p>Option is: {selected ? "selected" : "not selected"}</p>
// this button toggles the active state
<button onClick={() => setSelected(!selected)}>Toggle</button>
</>
)
}
This question already has answers here:
Access child state of child from parent component in react
(3 answers)
Closed last year.
I have a React element that renders Child elements with a target state. this target state can change anytime and parent doesn't have access at the moment.
const Parent = () => {
function getTarget(){
//TODO
}
return(
<Button>get target</Button>
{children.map(c=>{
<Child props={props}/>
})}
)
}
const Child = (props) => {
//props stuff
const [target, setTarget] = useState(null)
// this target would be changed by user as they interact.
return(
//child elements
)
}
what I'm trying to do is to get the target state of the Child using button in the Parent with following restraints:
There can be variable amount of Child elements, but only one of them are visible at a time.
The "get target" button has to be in Parent, the "target" state has to be initialized in child, and it's unknown.
because only on Child is active at a time, a solution that works for
return(
<Button>get target</Button>
<Child props={props}/>
)
is also fine.
const Parent = () => {
const [activeTarget, setActiveTarget] = useState(null);
const handleButton = () => {
console.log(activeTarget);
}
return(
<Button onClick={handleButton}>get target</Button>
{children.map(c=>{
<Child setActiveTarget={setActiveTarget} />
})}
)
}
const Child = ({setActiveTarget}) => {
const [target, setTarget] = useState(null);
// when the user interacts call 'setTarget' and 'setActiveTarget' to update both states
// update parent state when child mounts
useEffect(() => {
setActiveTarget(target);
}, [target]} // you can additionally add dependencies to update the parent state conditionally
return(
//child elements
)
}
I have a PlayArea component with a number of Card components as children, for a card game.
The position of the cards is managed by the PlayArea, which has a state value called cardsInPlay, which is an array of CardData objects including positional coordinates among other things. PlayArea passes cardsInPlay and setCardsInPlay (from useState) into each Card child component.
Cards are draggable, and while being dragged they call setCardsInPlay to update their own position.
The result, of course, is that cardsInPlay changes and therefore every card re-renders. This may grow costly if a hundred cards make it out onto the table.
How can I avoid this? Both PlayArea and Card are functional components.
Here's a simple code representation of that description:
const PlayArea = () => {
const [cardsInPlay, setCardsInPlay] = useState([]);
return (
<>
{ cardsInPlay.map(card => (
<Card
key={card.id}
card={card}
cardsInPlay={cardsInPlay}
setCardsInPlay={setCardsInPlay} />
}
</>
);
}
const Card = React.memo({card, cardsInPlay, setCardsInPlay}) => {
const onDrag = (moveEvent) => {
setCardsInPlay(
cardsInPlay.map(cardInPlay => {
if (cardInPlay.id === card.id) {
return {
...cardInPlay,
x: moveEvent.clientX,
y: moveEvent.clientY
};
}
return cardInPlay;
}));
};
return (<div onDrag={onDrag} />);
});
It depends on how you pass cardsInPlay to each Card component. It doesn't matter if the array in state changes as long as you pass only the required information to child.
Eg:
<Card positionX={cardsInPlay[card.id].x} positionY={cardsInPlay[card.id].y} />
will not cause a re-render, because even i the parent array changes, the instance itself is not getting a new prop. But if you pass the whole data to each component :
<Card cardsInPlay={cardsInPlay} />
it will cause all to re-render because each Card would get a new prop for every render as no two arrays,objects are equal in Javascript.
P.S : Edited after seeing sample code
The problem is you're passing the entire cardsInPlay array to each Card, so React.memo() will still re-render each card because the props have changed. Only pass the element that each card needs to know about and it will only re-render the card that has changed. You can access the previous cardsInPlay using the functional update signature of setCardsInPlay():
const PlayArea = () => {
const [cardsInPlay, setCardsInPlay] = useState([]);
const cards = cardsInPlay.map(
card => (
<Card
key={card.id}
card={card}
setCardsInPlay={setCardsInPlay} />
)
);
return (<>{cards}</>);
};
const Card = React.memo(({ card, setCardsInPlay }) => {
const onDrag = (moveEvent) => {
setCardsInPlay(
cardsInPlay => cardsInPlay.map(cardInPlay => {
if (cardInPlay.id === card.id) {
return {
...cardInPlay,
x: moveEvent.clientX,
y: moveEvent.clientY
};
}
return cardInPlay;
})
);
};
return (<div onDrag={onDrag} />);
});
TL;DR This is my Parent component:
const Parent = () => {
const [open, setOpen] = useState([]);
const handleExpand = panelIndex => {
if (open.includes(panelIndex)) {
// remove panelIndex from [...open]
// asign new array to variable: newOpen
// set the state
setOpen(newOpen);
} else {
setOpen([...open, panelIndex]);
}
}
return (
<div>
<Child expand={handleExpand} /> // No need to update
<Other isExpanded={open} /> // needs to update if open changed
</div>
)
}
And this is my Child component:
const Child = (props) => (
<button
type="button"
onClick={() => props.expand(1)}
>
EXPAND PANEL 1
</button>
);
export default React.memo(Child, () => true); // true means don't re-render
Those code are just an example. The main point is I don't need to update or re-render Child component because it just a button. But the second time I click the button it's not triggering Parent to re-render.
If I put console.log(open) inside handleExpand like so:
const handleExpand = panelIndex => {
console.log(open);
if (open.includes(panelIndex)) {
// remove panelIndex from [...open]
// asign new array to variable: newOpen
// set the state
setOpen(newOpen);
} else {
setOpen([...open, panelIndex]);
}
}
it printed out the same array everytime I clicked the button as if the value of open which is array never updated.
But if I let <Child /> component re-render when open changed, it works. Why is that? is this something as expected?
This is indeed expected behavior.
What you are experiencing here are function closures. When you pass handleExpand to Child all referenced variables are 'saved' with their current value. open = []. Since your component does not re-render it will not receive a 'new version' of your handleExpand callback. Every call will have the same result.
There are several ways of bypassing this. First obviously being letting your Child component re-render.
However if you strictly do not want to rerender you could use useRefwhich creates an object and access it's current property:
const openRef = useRef([])
const [open, setOpen] = useState(openRef.current);
// We keep our ref value synced with our state value
useEffect(() => {
openRef.current = open;
}, [open])
const handleExpand = panelIndex => {
if (openRef.current.includes(panelIndex)) {
setOpen(newOpen);
} else {
// Notice we use the callback version to get the current state
// and not a referenced state from the closure
setOpen(open => [...open, panelIndex]);
}
}