Home > Software design >  How to use useEffect/state/variables properly without user interaction?
How to use useEffect/state/variables properly without user interaction?

Time:08-08

My goal is to set up a game loop but a simple test isn't working as expected. In the following component, I am trying the useEffect hook to increment food. I expect to see "Food: 1". Instead I see "Food: 0". When I inspect the component with the dev tools, I can see that food is 2. I've discovered that the component mounts, increments food, unmounts, mounts again and increments food once more.

I have two questions:

  1. Can I do something about the double mount? (like prevent it or wait until the final mount with a nested component perhaps?)
  2. Why does the displayed food count still equal zero? Is it because game inside <span>Food: {game.food}</span> still refers to the initial instance? If so, how do I get the latest instance?

Component:

import React from "react";

class Game {
  food = 0;
}

export default function App() {
  const [game, setGame] = React.useState(new Game());

  React.useEffect(() => {
    setGame((game) => {
      game.food  = 1;
      return game;
    });
  });

  return <span>Food: {game.food}</span>;
}

CodePudding user response:

Don't Mutate State Objects

React uses reference comparisons and expects the reference of the root state object to change if any data within it has changed.

For Example:

// DON'T
setGame((game) => {
    // mutate and return same object
    game.food  = 1;
    return game;
});

// DO
setGame((current) => {
    // create new object with updated food value
    return {
        ...current,
        food: current.food   1
    };
});

Using the same reference will cause components to not update as expected.

useEffect Dependency Array

A useEffect without a dependency array will trigger every time the component renders.

If you wish for the useEffect to only trigger on mount provide an empty dependency array.

For Example:

// Every Render
useEffect(() => {
    alert('I trigger every render');
});

// On Mount
useEffect(() => {
    alert('I trigger on mount');
}, []);

// Everytime the reference for game changes
useEffect(() => {
    alert('I trigger everytime the game state is update');
}, [game]);

CodePudding user response:

Conclusion

  1. "Mount twice" probably you are using react 18 and have strict mode enabled. It will trigger useEffect twice in dev mode from docs
  2. If you want to update the view, you should make the reference of the game variable changes (instead of changing its attrs).

Solution

const initialGame = {
  food: 0
}

export default function App() {
  const [game, setGame] = React.useState(initialGame);

  React.useEffect(() => {
    setGame((game) => {
      game.food  = 1;
      return {...game};
    });
  }, []);

  return <span>Food: {game.food}</span>;
}

CodePudding user response:

  1. No you should not useEffect as a loop, its execution depends on your component states and its parent component, so this leaves 3 solutions 1st while loop, 2nd requestAnimationFrame and 3rd setInterval. while loop is discouraged because it will block event loop and canceling/stopping can be tedious.
  2. double mount ? i think its react double checking function, which does this only dev mode. Once you switch to requestAnimationFrame you won't be having that issue.
  3. use tried mutate state and react doesn't recognizes this so it doesn't re render. solution: return new object.

updating states

useEffect(() => {
  setGame((current) => {
    const newState = { ...current, food: current.food   1 }
    return newState
  })
}, [])

using setInterval to act as loop

useEffect(() => {
  const id = setInterval(() => setCount((count) => count   1), 1000)
  return () => clearInterval(id)
}, [])

using requestAnimationFrame to act as loop

// credit: https://css-tricks.com/using-requestanimationframe-with-react-hooks/
const requestRef = React.useRef()

const animate = (time) => {
  setCount((count) => count   1)
  requestRef.current = requestAnimationFrame(animate)
}

useEffect(() => {
  requestRef.current = requestAnimationFrame(animate)
  return () => cancelAnimationFrame(requestRef.current)
}, []) // Make sure the effect runs only once
  • Related