skip to Main Content

I am getting different behavior between the two code blocks when I believe they should be the same. My goal is to update compass heading using watchHeadingAsync and perform and animation from the previous heading to the new heading.

The first code block results in heading[0] always being 0 and heading[1] being the current heading. The second block performs as expected. Is there some race condition I am unaware of that is causing this? Any insight would be greatly appreciated!

const [heading, setHeading] = useState([0,0]);

useEffect(() => {
    Location.watchHeadingAsync((headObj) => {
        let h = Math.round(headObj.magHeading) * -1;
        setHeading([heading[1], h]);
    });
}, []);

console.log(heading[0], '->', heading[1]);
const [heading, setHeading] = useState([0,0]);
const oldHeading = useRef(0);
const newHeading = useRef(0);

useEffect(() => {
    Location.watchHeadingAsync((headObj) => {
        let h = Math.round(headObj.magHeading) * -1;
        oldHeading.current = newHeading.current;
        newHeading.current = h;
        setHeading([oldHeading.current, newHeading.current]);
    });
}, []);

console.log(heading[0], '->', heading[1]);

3

Answers


  1. The heading variable inside the useEffect callback is the constant value from when the effect ran. Indeed, it’s always [0, 0] and never changes. To fix this stale closure problem, you want to use the updater callback variant of the state setter:

    useEffect(() => {
        Location.watchHeadingAsync((headObj) => {
            let h = Math.round(headObj.magHeading) * -1;
            setHeading(prevHeading => [prevHeading[1], h]);
        });
    }, []);
    

    Do not use refs as a workaround for this.

    Login or Signup to reply.
  2. The reason you’re observing different behavior between the two code blocks is indeed related to a race condition.
    In this block,

    const [heading, setHeading] = useState([0,0]);
    
    useEffect(() => {
        Location.watchHeadingAsync((headObj) => {
            let h = Math.round(headObj.magHeading) * -1;
            setHeading([heading[1], h]);
        });
    }, []);
    

    you’re setting the heading state based on the current value of heading, but since setHeading is asynchronous, it won’t immediately update the state.
    in the second block, using useRef ensures that you capture the correct previous heading value before updating the state, thus producing the expected behavior.

    Login or Signup to reply.
  3. The difference in behavior between the two code blocks stems from how you’re managing state updates in the useEffect hook. Let’s break down the issue:

    Code Block 1:

    • You’re using a single state variable heading as an array to store both the old and new headings.
    • Inside useEffect, you update the state using setHeading([heading[1], h]). This creates a new array with the previous heading at index 1 and the new heading at index 0.
    • However, React performs state updates asynchronously. So, when you call setHeading, the state might not be updated immediately. In the next render, you log the heading state, which might still have the old values. This explains why heading[0] is always 0.

    Code Block 2:

    • You’re using two separate state variables: oldHeading and newHeading.
    • Inside useEffect, you update oldHeading.current and newHeading.current directly. These are ref values, which hold a mutable reference to a value. So, the changes are reflected immediately.
    • Then, you update the heading state using setHeading([oldHeading.current, newHeading.current]). This ensures you’re setting the state with the latest values from the refs.

    I don’t know whether the issue in the first block is technically a race condition or not, but it is an asynchronous update behavior of React’s state management (useState()).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search