React.useState does not reload state from props
Asked Answered
H

8

246

I'm expecting state to reload on props change, but this does not work and user variable is not updated on next useState call, what is wrong?

function Avatar(props) {
  const [user, setUser] = React.useState({...props.user});
  return user.avatar ? 
         (<img src={user.avatar}/>)
        : (<p>Loading...</p>);
}

codepen

Haematogenesis answered 25/2, 2019 at 12:1 Comment(1)
Is that all the code in the component or it's shortened for simplicity? As it is, it makes no sense to use an intermediate state instead of simply props.user.Hubbub
E
84

I've seen almost all the answers to this question promoting a bad pattern: updating state as a result of a prop change inside a useEffect call. The useEffect hook is used for synchronizing your React components with external systems. Using it for synchronizing React states can potentially lead to bugs (because re-renders caused by other effects can lead to unintended state updates). A better solution would be to trigger a reconciliation with a key prop change in the <Avatar /> component from its parent:

// App.jsx
function App() {
   // ...logic here
   return <Avatar initialUser={user} key={user.id} />
}

// Avatar.jsx
function Avatar({ initialUser }) {
 // I suppose you need this component to manage it's own state 
 // otherwise you can get rid of this useState altogether.
  const [user, setUser] = React.useState(initialUser);
  return user.avatar ? (
    <img src={user.avatar} />
  ) : (
    <p>Loading...</p>
  );
}

You can think of that key prop in this case as the dependency array of useEffect, but you won't be triggering unintended state changes as a result of unexpected useEffect calls triggered by the component renders.

You can read more about this here: Putting Props To State

And more info on how useEffect might be a foot gun, here:

You Might Not Need an Effect

Eduard answered 8/7, 2022 at 17:0 Comment(10)
Thank you! "You Might Not Need an Effect" is a gem!Gumwood
Upvoted. This approach is good if you have a specific key for child. I tried enabling Chrome Paint Flash, noticed green rectangles for full child, not just the changed element. I feel like going back to useEffect.Velure
I would say that if you have measured performance and it drastically affects the rendering behavior, you may consider opting in for some optimizations like memoization via React.memo (there's also 'conditionally rendering' or 'lifting up the state' as mentioned in the "Putting props to State" article). I'm not sure that trading possible UI inconsistencies for a slight win on performance is a good tradeoff. I've found out that useEffect is where most of the times violations of idempotence occur, so I feel like this wave of "avoiding useEffect as much as you can" is the right path forward.Eduard
Thank you! Helped me solve an issue I have been having for hoursHercule
Actually I change the way to use key but it still not workingKiwanis
@AlejandroRodriguezP. how do you deal with if the props we dependent on is an array of objects? For me I am using an AG Grid component that expects an array of rows. And it won’t update the ui if I don’t update the rows. So I have to use useEffect if I want to update grid’s rows and I get new rows as props from parent.Spray
If I understood your question correctly, in this particular case I think I would lift the state of your AG Grid component to the parent, and pass the state dispatcher - setUsers for example, from the parent to the <AGGrid /> child component. Maybe initializing state in AG Grid component from props is not a good fit for that case, because you can't use an array as key. But it's also not a suitable case for useEffect because it's rerendering a component based on props given by the parent, which it's not an effect. See possible example hereEduard
What if we need a stable key as recommended by the React documentation when rendering a list of similar components?Zealand
It's the same, if you are rendering a list of elements from an array of data, and each element of such data has an id property, the behavior remains the same. Your Components will change if the data change, and therefore rerender. The keys are unstable if you start doing things like Math.random()Eduard
Brilliant answer! Thanks a lot. Never thought about that solution!Eterne
H
440

The argument passed to useState is the initial state much like setting state in constructor for a class component and isn't used to update the state on re-render

If you want to update state on prop change, make use of useEffect hook

function Avatar(props) {
  const [user, setUser] = React.useState({...props.user});

  React.useEffect(() => {
      setUser(props.user);
  }, [props.user])

  return user.avatar ? 
         (<img src={user.avatar}/>)
        : (<p>Loading...</p>);
}

Working demo

Huddle answered 25/2, 2019 at 12:18 Comment(14)
When setting initial state in constructor its clear to understand that constructor is called once on class instance creation. With hooks it looks like useState is called each time on function call and each time it should initialize state, but some unexplained "magic" happen there.Haematogenesis
Maybe this post shed some light on it #54673688Huddle
if you need to merge props into state, you get stuck in an infinite loop when using create-react-app which requires state to be in the dependency arrayWalkyrie
This ends up setting the initial state twice which is pretty annoying. I wonder if you can set the initial state to null and then use React.useEffect to set the initial state at each time.Tawana
This doesn't work if state is also controlling rendering. I have a functional component that renders based on props and renders based on state. If the state tells the component to render because it changed, then the useEffect runs and sets state back to the default value. This is really bad design on their part.Flavorous
I've been to the same case @GregVeres mentioned - I wanted to create an optimistic switch button. Should be so simple, but with the new hooks, React 16.3 deprecated the ways we could do this in a clean fashion. The solution is a documented hack: force React to recreate your component by linking its key with whatever is going to change inside of it. This way you're guaranteed to reset the state when the relevant props change! Not sure if I laugh or cry.Tawnyatawsha
@Tawnyatawsha The state only sets back to props value changes if the props.user changes and not otherwiseHuddle
@Tawana you can use a ref to skip the initial render after the component mounted: #53179575Hansel
Why are you using React.useState({...props.user}) instead of just React.useState(props.user)?Larousse
Fallen into the same trap of endless component update cycle when rendering depended on props and state. Solved by separating the component into an outer and inner component. Outer component calculates initial state for inner component based only on outer props. Initial state then is passed to the inner component via prop and used to initialize inner's state. Updating inner states when doesn't cause an endless loop, yet changing outer props correctly reinitializes inner.Luz
Then what is the difference of using the lazy version of React.useState which accepts a lambda?Ploughshare
Can you explain what is the need to use useState inside the avatar component since it rely on the passed props and never changed inside the component ? Why not use directly props.user ? The component gets re-render even when props change.Matz
@AntonioPantano, you can directly use props.user when the child component is not locally modifying the props temporarily. The above solution is for demonstrating how you can handle it if there is a requirement similar to this.Huddle
@Ploughshare No functional difference, it's just a way to memoize the initial value (the lambda will be called only once), so it's sseful when that initial value is computationally heavy.Hubbub
E
84

I've seen almost all the answers to this question promoting a bad pattern: updating state as a result of a prop change inside a useEffect call. The useEffect hook is used for synchronizing your React components with external systems. Using it for synchronizing React states can potentially lead to bugs (because re-renders caused by other effects can lead to unintended state updates). A better solution would be to trigger a reconciliation with a key prop change in the <Avatar /> component from its parent:

// App.jsx
function App() {
   // ...logic here
   return <Avatar initialUser={user} key={user.id} />
}

// Avatar.jsx
function Avatar({ initialUser }) {
 // I suppose you need this component to manage it's own state 
 // otherwise you can get rid of this useState altogether.
  const [user, setUser] = React.useState(initialUser);
  return user.avatar ? (
    <img src={user.avatar} />
  ) : (
    <p>Loading...</p>
  );
}

You can think of that key prop in this case as the dependency array of useEffect, but you won't be triggering unintended state changes as a result of unexpected useEffect calls triggered by the component renders.

You can read more about this here: Putting Props To State

And more info on how useEffect might be a foot gun, here:

You Might Not Need an Effect

Eduard answered 8/7, 2022 at 17:0 Comment(10)
Thank you! "You Might Not Need an Effect" is a gem!Gumwood
Upvoted. This approach is good if you have a specific key for child. I tried enabling Chrome Paint Flash, noticed green rectangles for full child, not just the changed element. I feel like going back to useEffect.Velure
I would say that if you have measured performance and it drastically affects the rendering behavior, you may consider opting in for some optimizations like memoization via React.memo (there's also 'conditionally rendering' or 'lifting up the state' as mentioned in the "Putting props to State" article). I'm not sure that trading possible UI inconsistencies for a slight win on performance is a good tradeoff. I've found out that useEffect is where most of the times violations of idempotence occur, so I feel like this wave of "avoiding useEffect as much as you can" is the right path forward.Eduard
Thank you! Helped me solve an issue I have been having for hoursHercule
Actually I change the way to use key but it still not workingKiwanis
@AlejandroRodriguezP. how do you deal with if the props we dependent on is an array of objects? For me I am using an AG Grid component that expects an array of rows. And it won’t update the ui if I don’t update the rows. So I have to use useEffect if I want to update grid’s rows and I get new rows as props from parent.Spray
If I understood your question correctly, in this particular case I think I would lift the state of your AG Grid component to the parent, and pass the state dispatcher - setUsers for example, from the parent to the <AGGrid /> child component. Maybe initializing state in AG Grid component from props is not a good fit for that case, because you can't use an array as key. But it's also not a suitable case for useEffect because it's rerendering a component based on props given by the parent, which it's not an effect. See possible example hereEduard
What if we need a stable key as recommended by the React documentation when rendering a list of similar components?Zealand
It's the same, if you are rendering a list of elements from an array of data, and each element of such data has an id property, the behavior remains the same. Your Components will change if the data change, and therefore rerender. The keys are unstable if you start doing things like Math.random()Eduard
Brilliant answer! Thanks a lot. Never thought about that solution!Eterne
E
20

Functional components where we use useState to set initial values to our variable, if we pass initial value through props, it will always set same initial value until you don't make use of useEffect hook,

for example your case this will do your job

 React.useEffect(() => {
      setUser(props.user);
  }, [props.user])

The function passed to useEffect will run after the render is committed to the screen.

By default, effects run after every completed render, but you can choose to fire them only when certain values have changed.

React.useEffect(FunctionYouWantToRunAfterEveryRender)

if you pass only one argument to useEffect it will run this method after every render you can decide when to fire this FunctionYouWantToRunAfterEveryRender by passing second argument to useEffect

React.useEffect(FunctionYouWantToRunAfterEveryRender, [props.user])

as you notice i am passing [props.user] now useEffect will only fire this FunctionYouWantToRunAfterEveryRender function when props.user is changed

i hope this helps your understanding let me know if any improvements are required thanks

Exum answered 19/7, 2020 at 16:15 Comment(0)
I
9

you can create your own custom simple hooks for this. The hooks changed default value when it changed.

https://gist.github.com/mahmut-gundogdu/193ad830be31807ee4e232a05aeec1d8

    import {useEffect, useState} from 'react';

    export function useStateWithDep(defaultValue: any) {
      const [value, setValue] = useState(defaultValue);
    
      useEffect(() => {
        setValue(defaultValue);
      }, [defaultValue]);
      return [value, setValue];
    }

#Example

const [user, setUser] = useStateWithDep(props.user);
Integral answered 8/6, 2021 at 19:34 Comment(2)
Let's improve the types: function useStateWithDep<T>(defaultValue: T), return [value, setValue] as const.Hubbub
Not that useful because useEffect callback will be called after the return statement. So at first it'll return old values.Emelun
D
4

I've created custom hooks like this:

const usePrevious = value => {
   const ref = React.useRef();

   React.useEffect(() => {
       ref.current = value;
   }, [value]);

   return ref.current;
}

const usePropState = datas => {
    const [dataset, setDataset] = useState(datas);
    const prevDatas = usePrevious(datas);

    const handleChangeDataset = data => setDataset(data);

    React.useEffect(() => {
        if (!deepEqual(datas, prevDatas)) // deepEqual is my function to compare object/array using deep-equal
            setDataset(datas);
    }, [datas, prevDatas]);

    return [
        dataset,
        handleChangeDataset
    ]
}

To use:

const [state, setState] = usePropState(props.datas);
Decane answered 29/1, 2021 at 7:47 Comment(1)
why are you creating an unstable (not using useCallback) handleChangeDataset instead of returning the stable setDataset instead?Allanson
F
2

The parameter passed to React.useState() is only the initial value for that state. React isn't going to recognize that as changing the state, only setting its default value. You'll want to set the default state initially, and then conditionally call setUser(newValue), which will be recognized as a new state, and re-render the component.

I would recommend caution updating state without some kind of condition to keep it from constantly updating, and therefore re-rendering everytime props are received. You may want to consider hoisting the state functionality to a parent component and passing the state of the parent down to this Avatar as a prop.

Ferrara answered 25/2, 2019 at 12:9 Comment(0)
P
0

you can avoid using useEffect to update local state with updated props

here an example how you can do it:

With useEffect:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // 🔴 Avoid: Adjusting state on prop change in an Effect
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

without useEffect:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // Better: Adjust the state while rendering
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

you can read more about that in this react documentation: https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes

Pontifex answered 30/1 at 11:17 Comment(0)
A
-5

According to the ReactJS documentation about Hooks :

But what happens if the friend prop changes while the component is on the screen? Our component would continue displaying the online status of a different friend. This is a bug. We would also cause a memory leak or crash when unmounting since the unsubscribe call would use the wrong friend ID.

Your only interaction here should happen on a props change, which seems not to work. You could (still according to the doc) use a componentDidUpdate(prevProps) to proactively catch any update to the props.

PS : I don't have enough code to judge, but couldn't you actively setUser() inside your Avatar(props) function ?

Arsonist answered 25/2, 2019 at 12:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.