I have a component that doesn't appear to re-render even though the parent state has in fact updated.
Here is the (contrived) code:
const Index = () => {
render (
<Foo initialFoo={{ isTrue: true, count: 0 }} />
)
}
const Foo = ({ initialFoo }) => {
const [foo, setFoo] = useState({ ...initialFoo })
const reset = () => {
setFoo({ ...initialFoo })
}
return (
<>
{
foo.isTrue ?
<Bar initialBar={foo} reset={reset} /> :
<span>{foo.count}</span>
}
</>
)
}
const Bar = ({ initialBar, reset }) => {
const [bar, setBar] = useState({ ...initialBar })
return (
<div
onm ouseOver={() => setBar({ ...bar, count: bar.count 1 })}
onClick={reset}
>
<span>{bar.count}</span>
</div>
)
}
Hovering over the component increments count
by 1 and displays it accordingly, but when I click on it, nothing changes when I expect the initial state to be displayed.
If I add logs in the Foo
component, I can see that foo
is updating back to the initial state after clicking on the component, but Bar
isn't re-rendering even though it depends on foo
. Shouldn't a change in state in the parent component trigger the children to re-render?
Note: I'm not looking for suggestions on how to write the code better/differently to achieve the desired result, rather just trying to understand the reason why the component is not displaying the original state after clicking on it in this current setup
CodePudding user response:
When you declare this state:
const [bar, setBar] = useState({ ...initialBar })
You are telling React to start tracking a new state variable which is initialised with the current value of initialBar
. When the value of initialBar
changes in future renders, it will have no effect on the state derived from it - the initialisation only happens on component mount.
To keep bar
synchronised with initialBar
, you would need to add a useEffect
and manually set bar
yourself:
const [bar, setBar] = useState({ ...initialBar })
useEffect(() => {
setBar(initialBar)
}, [initialBar])
This effect will be run only when the reference value of initialBar
changes, which is currently only when the reset
function is called. You would also effectively be setting bar
twice on component mount.
CodePudding user response:
Everything works as expected, reset
drops foo
to initail state but in you Bar component initialBar is used only to populate the initial state and it will not update after that.
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
return <Index />;
}
const Index = () => {
return <Foo initialFoo={{ isTrue: true, count: 0 }} />;
};
const Foo = ({ initialFoo }) => {
const [foo, setFoo] = useState({ ...initialFoo });
const reset = () => {
console.log(foo, initialFoo);
setFoo({ ...initialFoo });
};
return (
<>
{foo.isTrue ? (
<Bar initialBar={foo} reset={reset} />
) : (
<span>{foo.count}</span>
)}
</>
);
};
const Bar = ({ initialBar, reset }) => {
const [bar, setBar] = useState({ ...initialBar });
// Uncomment to update bar on initialBar changed
/*
useEffect(() => {
setBar({ ...initialBar });
}, [initialBar]);
*/
return (
<button
onm ouseOver={() => setBar({ ...bar, count: bar.count 1 })}
onClick={reset}
>
<span>{bar.count}</span>
</button>
);
};