Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: component is rendered before it should. #21612

Closed
jordease opened this issue Jun 3, 2021 · 7 comments
Closed

Bug: component is rendered before it should. #21612

jordease opened this issue Jun 3, 2021 · 7 comments
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@jordease
Copy link

jordease commented Jun 3, 2021

Hi,

I am a bit confused about the example below, where I expect the state to be set before a render.

const Component1 = () => {
    const [state, setState] = useState();
    return <Component2 state={state} setState={setState}/>
}

const Component2 = ({state, setState}) => {
    if(state === undefined)
        setState({title: 'Hello'});     // shouldnt this be set before the render? it works if the useState hook is on this component

    return <Component3 state={state}/>
}

const Component3 = ({state}) => {
    // throws an error
    return <p>{state.title.toString()}</p>
}
@jordease jordease added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Jun 3, 2021
@jordease jordease closed this as completed Jun 3, 2021
@jordease jordease reopened this Jun 3, 2021
@joealden
Copy link

joealden commented Jun 4, 2021

@jordease, setState is asynchronous, which means that Component3 throws an error because state hasn't yet been "set" to the value passed in Component2. The reasoning to why setState is asynchronous can be found here: #11527 (comment).

@billyjanitsch
Copy link
Contributor

@joealden, that's not correct. When setState is called during render, the update is flushed before React descends into the subtree. For example, as @jordease's comment mentions, the following works correctly:

const Component1 = () => {
    const [state, setState] = useState();
    if(state === undefined) setState({title: 'Hello'});
    return <Component2 state={state} />
}

const Component2 = ({state}) => {
    return <p>{state.title.toString()}</p>
}

@jordease, the issue is that calling setState during render is only allowed in the special case where the state is owned by the component being rendered. Your snippet results in a console error explaining this:

Warning: Cannot update a component (Component1) while rendering a different component (Component2). To locate the bad setState() call inside Component2, follow the stack trace as described in https://reactjs.org/link/setstate-in-render

If you need to update the state in a descendent component, it has to be in an effect or an event handler.

@bvaughn
Copy link
Contributor

bvaughn commented Jun 5, 2021

@billyjanitsch's comment looks correct. Thanks!

Going to close this issue.

@bvaughn bvaughn closed this as completed Jun 5, 2021
@joealden
Copy link

joealden commented Jun 5, 2021

@billyjanitsch thanks for correcting the record! I overlooked that last part of the code comment, and I learnt something new today about React! 😃

@jordease
Copy link
Author

jordease commented Jun 8, 2021

@billyjanitsch I tried using useEffect to solve this but it didn't work as well.

const Component1 = () => {
    const [state, setState] = useState();
    return <Component2 state={state} setState={setState}/>
}

const Component2 = ({state, setState}) => {
    useEffect(()=>{
        setState({title: 'Hello'})
    },[state])

    return <Component3 state={state}/>
}

const Component3 = ({state}) => {
    // throws an error
    return <p>{state.title.toString()}</p>
}

I guess the way to this to would be to update state manually for the same render so something like:

if(state === undefined){
state = {...state, title: 'Hello'}
//or directly
state.title = 'Hello'

setState({title: 'Hello'})
}

@joealden
Copy link

joealden commented Jun 8, 2021

I tried using useEffect to solve this but it didn't work as well.

@jordease that is because useEffect calls run after render, meaning that state will still be undefined on initial render, hence why the error occurs in that case.

I guess the way to this to would be to update state manually for the same render so something like: [...]

@jordease I personally wouldn't advise doing this; why can you not just set a default state value in your case?

If you really can't set a default value where you define the state, I guess instead of manually mutating state as you've shown, you could create a "fallback" version of state in Component2 that you pass down to Component3 like so:

const Component1 = () => {
  const [state, setState] = useState();
  return <Component2 state={state} setState={setState} />;
};

const Component2 = ({ state, setState }) => {
  const stateFallback = { title: "Hello" };

  useEffect(() => {
    if (!state) setState(stateFallback);
  }, [state]);

  return <Component3 state={state ?? stateFallback} />;
};

const Component3 = ({ state }) => {
  return <p>{state.title.toString()}</p>;
};

@jordease
Copy link
Author

jordease commented Jun 9, 2021

Because in this case Component3 will be rendered 2 times... it is solvable though if I dont use setState() and simply use the default value.

Thanks for the help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

4 participants