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;```
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:
useEffect
closure is invoked once on first render.change
event is triggered.setDimensions
inside your closure always prints the same value from the function outer scope orientation
variable (which at construction time has value null
).setOrientation
is invoked, React internally updates the state and triggers a new render cycle in the parent component.useDeviceDimensions
is invoked again from the parent component, it now has and returns the new orientation
state.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.
useEffect
in a safe wayYour 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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With