237

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

Bill Keller
  • 793
  • 7
  • 22
vitalyster
  • 4,980
  • 3
  • 19
  • 27
  • 2
    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`. – tokland Jan 30 '22 at 20:22

7 Answers7

433

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

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • 62
    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. – vitalyster Feb 25 '19 at 12:31
  • Maybe this post shed some light on it https://stackoverflow.com/questions/54673188/how-does-react-implement-hooks-so-that-they-rely-on-call-order/54860720#54860720 – Shubham Khatri Feb 25 '19 at 12:32
  • 1
    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 array – user210757 Jul 29 '19 at 16:42
  • 4
    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. – CMCDragonkai Aug 13 '19 at 05:08
  • 9
    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. – Greg Veres Feb 09 '20 at 02:56
  • 1
    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](https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key): 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. – igorsantos07 Apr 28 '20 at 08:42
  • 2
    @igorsantos07 The state only sets back to props value changes if the props.user changes and not otherwise – Shubham Khatri Apr 28 '20 at 08:48
  • @CMCDragonkai you can use a ref to skip the initial render after the component mounted: https://stackoverflow.com/questions/53179075/with-useeffect-how-can-i-skip-applying-an-effect-upon-the-initial-render – Chunky Chunk Jul 11 '20 at 20:15
  • 1
    Why are you using `React.useState({...props.user})` instead of just `React.useState(props.user)`? – Robo Robok Nov 12 '20 at 22:49
  • 1
    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. – TheDiveO Dec 04 '20 at 19:55
  • Then what is the difference of using the lazy version of `React.useState` which accepts a lambda? – Ramesh Feb 01 '21 at 08:31
  • 1
    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. – Antonio Pantano Oct 17 '21 at 07:57
  • @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. – Shubham Khatri Oct 20 '21 at 09:10
  • 1
    @Ramesh 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. – tokland Jan 30 '22 at 20:20
63

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

Kevin Panko
  • 8,356
  • 19
  • 50
  • 61
  • 7
    Thank you! "You Might Not Need an Effect" is a gem! – jlh Jul 09 '22 at 14:09
  • 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. – kiranpradeep Aug 16 '22 at 11:17
  • 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. – Alejandro Rodriguez P. Aug 17 '22 at 16:55
  • 1
    Thank you! Helped me solve an issue I have been having for hours – Reina Reinhart Mar 10 '23 at 22:58
  • Actually I change the way to use key but it still not working – huykon225 Apr 12 '23 at 04:01
  • @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. – Shailesh Vaishampayan Jun 20 '23 at 19:08
  • 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 `` 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 here](https://tsplay.dev/WK3aGW) – Alejandro Rodriguez P. Jun 21 '23 at 22:40
19

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

Hanzla Habib
  • 3,457
  • 25
  • 25
7

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);
Maifee Ul Asad
  • 3,992
  • 6
  • 38
  • 86
Mahmut Gundogdu
  • 543
  • 4
  • 12
  • 1
    Let's improve the types: `function useStateWithDep(defaultValue: T)`, `return [value, setValue] as const`. – tokland Jan 30 '22 at 20:16
  • Not that useful because useEffect callback will be called after the return statement. So at first it'll return old values. – Sumit Wadhwa Jun 19 '22 at 08:54
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);
Dharman
  • 30,962
  • 25
  • 85
  • 135
Oan
  • 41
  • 1
  • 1
    why are you creating an unstable (not using useCallback) handleChangeDataset instead of returning the stable setDataset instead? – philk May 09 '22 at 22:16
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.

Drowsy
  • 313
  • 1
  • 10
-4

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 ?

Mwak
  • 106
  • 6