Ash
Ash

Reputation: 896

useState React hook always returning initial value

locationHistory is always an empty array in the following code:

export function LocationHistoryProvider({ history, children }) {
  const [locationHistory, setLocationHistory] = useState([])

  useEffect(() => history.listen((location, action) => {
    console.log('old state:', locationHistory)
    const newLocationHistory = locationHistory ? [...locationHistory, location.pathname] : [location.pathname]
    setLocationHistory(newLocationHistory)
  }), [history])

  return <LocationHistoryContext.Provider value={locationHistory}>{children}</LocationHistoryContext.Provider>
}

console.log always logs []. I have tried doing exactly the same thing in a regular react class and it works fine, which leads me to think I am using hooks wrong.

Any advice would be much appreciated.

UPDATE: Removing the second argument to useEffect ([history]) fixes it. But why? The intention is that this effect will not need to be rerun on every rerender. Becuase it shouldn't need to be. I thought that was the way effects worked.

Adding an empty array also breaks it. It seems [locationHistory] must be added as the 2nd argument to useEffect which stops it from breaking (or no 2nd argument at all). But I am confused why this stops it from breaking? history.listen should run any time the location changes. Why does useEffect need to run again every time locationHistory changes, in order to avoid the aforementioned problem?

P.S. Play around with it here: https://codesandbox.io/s/react-router-ur4d3?fontsize=14 (thanks to lissitz for doing most the leg work there)

Upvotes: 2

Views: 3628

Answers (1)

cbdeveloper
cbdeveloper

Reputation: 31335

You're setting up a listener for the history object, right?

Assuming your history object will remain the same (the very same object reference) across multiple render, this is want you should do:

  • Set up the listener, after 1st render (i.e: after mounting)
  • Remove the listener, after unmount

For this you could do it like this:

useEffect(()=>{
  history.listen(()=>{//DO WHATEVER});
  return () => history.unsubscribe(); // PSEUDO CODE. YOU CAN RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[]);   // THIS EMPTY ARRAY MAKES SURE YOUR EFFECT WILL ONLY RUN AFTER 1ST RENDER

But if your history object will change on every render, you'll need to:

  • cancel the last listener (from the previous render) and
  • set up a new listener every time your history object changes.
useEffect(()=>{
  history.listen(()=>{//DO SOMETHING});
  return () => history.unsubscribe(); // PSEUDO CODE. IN THIS CASE, YOU SHOULD RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[history]);   // THIS ARRAY MAKES SURE YOUR EFFECT WILL RUN AFTER EVERY RENDER WITH A DIFFERENT `history` OBJECT

NOTE: setState functions are guaranteed to be the same instance across every render. So they don't need to be in the dependency array.

But if you want to access the current state inside of your useEffect. You shouldn't use it directly like you did with the locationHistory (you can, but if you do, you'll need to add it to the dependency array and your effect will run every time it changes). To avoid accessing it directly and adding it to the dependency array, you can do it like this, by using the functional form of the setState method.

setLocationHistory((prevState) => {
  if (prevState.length > 0) {
     // DO WHATEVER
  }
  return SOMETHING;  // I.E.: SOMETHING WILL BE YOUR NEW STATE
});

Upvotes: 6

Related Questions