Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

setState call inside a custom hook is not updating the state

I'm trying to set up a basic custom hook to return the device orientation in React Native. I have an example of functional code right beside this and I can't figure out why this won't work. I think it's a closure/scoping issue of some sort but clearly I don't know.

I know there are easier ways of getting the device orientation, but I want to know why this isn't working and how to make it work.

Current behavior: calling setOrientation does not change the value of orientation. Expected behavior: calling setOrientation updates the value of orientation.

Once this happens I think the hook will work properly, since it returns null every time Dimensions updates.

[UPDATE: edited for clarity]


import { Dimensions, } from 'react-native';

function useDeviceDimensions() {

  const [orientation, setOrientation] = useState(null);

  useEffect(() => {
    const setDimensions = (e) => {
      console.log('running', orientation); // null
      const {width, height} = e.window;
      setOrientation(width > height ? 'landscape' : 'portrait');
    };

    Dimensions.addEventListener('change', setDimensions);

    return () => Dimensions.removeEventListener('change', setDimensions);
  }, [])

  return orientation;
}

export default useDeviceDimensions;```
like image 937
Jamie Sauve Avatar asked Jan 25 '23 19:01

Jamie Sauve


1 Answers

Your example works ...

setOrientation will actually change the state value of orientation and useDeviceDimensions will return the changed orientation. The issue is rather where you placed the console.log statement - and you are right, that is related to the closure (and its scope) passed to useEffect.

useEffect is invoked only once with your defined callback. This callback is a closure and can access orientation from useState. At the time of the closure creation, orientation variable will have the value null. Because there is never a new closure created ([] dependencies in useEffect), it will always print the value null, when the contained change listener is triggered.

However, setOrientation inside setDimensions will dispatch and communicate the change to React. The flow is like this:

  1. The useEffect closure is invoked once on first render.
  2. Sometime later a change event is triggered.
  3. setDimensions inside your closure always prints the same value from the function outer scope orientation variable (which at construction time has value null).
  4. setOrientation is invoked, React internally updates the state and triggers a new render cycle in the parent component.
  5. useDeviceDimensions is invoked again from the parent component, it now has and returns the new orientation state.
  6. A new change is triggered, go on with 3 again.

Point 4 works in the closure, because React has an internal list of “memory cells” for each component, where it can read and update the recent state, so updates work across the closure scope (see here for more info). The read side orientation is a stale variable, if you will, that just has a reference to the first state value, that React stored initially.

... but better declare useEffect in a safe way

Your useEffect hook uses state you don't mention in its dependencies array. What React docs say concerning this:

If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders. Link

So if you also want to use orientation in useEffect, you could declare it as dependency:

 useEffect(() => {
  ...
  }, [orientation]);

Have a look at this Codesandbox for an example, hope it clarifies things a bit!

like image 130
ford04 Avatar answered Jan 28 '23 09:01

ford04