154
votes

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

6

6 Answers

333
votes

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

11
votes

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

2
votes

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.

1
votes

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);
1
votes

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

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);

-3
votes

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 ?