Ariel
Ariel

Reputation: 1436

useState not updated as expected

I have two components, one placed inside the other. The children component prints the state passed by the parent and updates the state once with useEffect. In the parent component I'm using setState (an object with boolean values) and passing the state to all the children.

Parent component:

export default function App() {
  const [state, setState] = useState({
    one: false,
    two: false,
    three: false
  });
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <ChildComponent
        value={state.one}
        setState={(val) => setState({ ...state, one: val })}
      />
      <ChildComponent
        value={state.two}
        setState={(val) => setState({ ...state, two: val })}
      />
      <ChildComponent
        value={state.three}
        setState={(val) => setState({ ...state, three: val })}
      />
    </div>
  );
}

Child Component:

import { useEffect } from "react";

export default function ChildComponent({ value, setState }) {
  useEffect(() => {
    console.log("updated");
    setState(!value);
  }, []);
  return (
    <>
      <p>{value ? "true" : "false"}</p>
    </>
  );
}

Both components are really simple, and I'm expecting to see all boolean values being true (because the child components are modifying the state). The problem is that only the last item on the state is being updated. Can someone explain what is wrong with my code? A working example

Upvotes: 3

Views: 1750

Answers (4)

iam_atul22
iam_atul22

Reputation: 127

see what is happening here is, when you are sending the state in the first child component, the state.one is updated to true , but then, you move on to the second component, that is using the same previous state where state.one was false and the same happens in the third case too.

so what you need to do is pass a callback of the new updated state to the function setState.

setState= {(val)=>{setState(currState => ({ ...currState, two: val })}}

Upvotes: 0

Eliav Louski
Eliav Louski

Reputation: 5284

Edit: you can handle it as @koralarts suggested, but if you want to understand why it happened,read this.

let's explore what's happens in your example.

TLDR: you have to split the setState call to separate hook in the parent for each child component, or manage the state in the children if possible.
each time you calling setState in the effect react schedules this call, and not execute it directly. you are calling 3 times the same setState function in the same render, and each time react schedules the call to the next render, therefore, overriding the previous schedule.

If possible, manage this state from the children, if not, use different useState for each child on the parent.

Explanetetion:

let's be aware of the different lifecycle of a react component:

  • update call
  • effects (useLayoutEffect and later useEffect)

you can read the complete explanation here(highly recommended).

see this sandbox(like yours with logs).

read the difference between 'render' and 'update' here:

render - React tree is created based on the current state, then the tree is passed to the renderer that will update the VDOM, and changes will be flushed into the browser's DOM. render cycle consists of few phases that will be explained later. when we say that a component ' render' we are usually talking about the end of the render cycle, after changes are flushed into the browser's DOM.

update - when we say that a component 'updates', we are saying that the function component body re-executed (with possibly different props). is one of the phases of a render. multiple update cycles are possible during a render cycle. examples of the difference between update and render later.

now read this important notes:

Calling state hook from effect(like useEffect or useLayoutEffect) will cause React to schedule another render.

Calling state hook from FC body will cause React to schedule another update call.

your case:

  • update phase: parent
  • update phase: children
  • effect phase: children => setState call causing react to schedule another render on of the parent. this happens 3 times on each child in the same phase(useEffect) and each schedule overwrites the previous child's schedules.

Upvotes: 2

Naren
Naren

Reputation: 4480

I think is because asynchronous behaviour of setState, By the time it renders the ChildComponent, it still having the old state not the updated one. Like @koralarts answer, you have to use previous state to update the state.

Upvotes: 0

koralarts
koralarts

Reputation: 2112

The state update is dependent on the previous value of the state, you need to pass a callback as the argument for your setState instead.

You need to do this instead:

setState={(val) => setState(prev => ({ ...prev, one: val }))}

In your current implementation, you're spreading the initial value of the state where everything is false instead of the current value of the state.

Example: https://codesandbox.io/s/usestate-callback-1oq2n?file=/src/App.js

Upvotes: 2

Related Questions