score:8

Accepted answer

The reason why you did not get updated state is because you called it inside useEffect(() => {}, []) which is only called just once.

useEffect(() => {}, []) works just like componentDidMount().

When gameStart function is called, gamePlaytime is 100, and inside gameStart, it uses the same value however the timer works and the actual gamePlayTime is changed. In this case, you should monitor the change of gamePlayTime using useEffect.

...
  useEffect(() => {
      if (gamePlayTime % targetShowTime === 0) {
        const random = (Math.floor(Math.random() * 10000) % wp("70")) + wp("10");
        const targetPosition = { x: random, y: hp("90") };
        const spinInfoData = getspinArray()[Math.floor(Math.random() * 10) % 4];
        NewSpinShow(targetPosition, spinInfoData, spinSpeed);
      }
  }, [gamePlayTime]);

  const gameStart = () => {
    gameStartInternal = setInterval(() => {
      setGamePlayTime(t => t-1);
    }, 1000);
  };
...

score:4

You shouldn't use setInterval with hooks. Take a look at what Dan Abramov, one of the maintainers of React.js, said regarding an alternative on his blog: https://overreacted.io/making-setinterval-declarative-with-react-hooks/

score:5

https://overreacted.io/making-setinterval-declarative-with-react-hooks/

Dan Abramov article explain well how to works with hooks, state, and the setInterval() type of api!

Dan Abramov! Is one of the React maintaning team! So known and i pesonally love him!

Quick explanation

The problem is the problem of how to access state with a useEffect() that execute only once (first render)!

The short answer is: by the use of refs (useRef)! And another useEffect() that run again when update is necessary! Or at each render!

Let me explain! And check the Dan Abramov solution! And you'll get better the statement above at the end! With a second example that is not about setInterval()!

=>

useEffect() either run once only, or run in each render! or when the dependency update (when provided)!

Accessing state can be possible only through a useEffect() that run and render each relevant time!

Or through setState((state/*here the state*/) => <newStateExpression>)

But if you want to access the state inside useEffect() => rerun is necessary! meaning passing and executing the new callback!

That doesn't work well with setInterval! If you set it each time! the counter get reset! Leading to no execution if the component is re-rendering fast!

If you render only once! The state is not updated! As the first run, run a one callback! And make a closure! The state is fixed! useEffect(() => { <run once, state will stay the same> setInterval(() => { <state fixed> }) }, []).

For all such kind of situation! We need to use useRef! (refs)!

Save to it a callback that hold the state! From a useEffect() that rerender each time! Or by saving the state value itself in the ref! Depending on the usage!

Dan abramov solution for setInterval (simple and clean)

That's what you are looking for!

useInteval hook (by Dan Abramov)

import React, { useState, useEffect, useRef } from 'react';

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

Usage

import React, { useState, useEffect, useRef } from 'react';

function Counter() {
  let [count, setCount] = useState(0);

  useInterval(() => {
    // Your custom logic here
    setCount(count + 1);
  }, 1000);

  return <h1>{count}</h1>;
}

We can see how he kept saving the new callback at each re-render! A callback that contain the new state!

To use ! it's a clean simple hook! That's a beauty!

Make sure to read Dan article! As he explained and tackled a lot of things!

setState()

Dan Abramov mentioned this in his article!

If we need to set the state! Within a setInteral! One can use simply setState() with the callback version!

useState(() => {
   setInterval(() => {
      setState((state/*we have the latest state*/) => {
          // read and use state
          return <newStateExpression>;
      })
   }, 1000);
}, []) // run only once

we can even use that! Even when we are not setting state! Possible! Not good though! We just return the same state value!

setState((state) => {
   // Run right away!
   // access latest state
   return state; // same value (state didn't change)
});

However this will make different react internal code part to run (1,2,3), And checks! Which end by bailing out from re-rendering! Just fun to know!

We use this only for when we are updating the state! If not ! Then we need to use refs!

Another example: useState() with getter version

To show case the how to work with refs and state access! Let's go for another example! Here another pattern! Passing state in callbacks!

import React from 'react';

function useState(defaultVal) {
    // getting the state
    const [state, setState] = React.useState(defaultValue);
    // state holding ref
    const stateRef = React.useRef();
    stateRef.current = state; // setting directly here!
    // Because we need to return things at the end of the hook execution
    // not an effect

    // getter
    function getState() {
       // returning the ref (not the state directly) 
       // So getter can be used any where!
       return stateRef.current;
    } 
    
    return [state, setState, getState];
}

The example is of the same category! But here no effect!

However we can use the above hook to access state in the hook simply as bellow!

const [state, useState, getState] = useState(); // Our version! not react
// ref is already uptated by this call

React.useEffect(() => {
  setInteval(() => {
      const state = getState();
      // do what you want with the state!
      // it works because of the ref! Get State return a value to the same ref!
     // which is already updated 
  }, 1000)
}, []); // running only once

For setInterval()! The good solution is Dan Abramov hook! Making a strong custom hook for a thing is the cool thing to do! This second example is more to showcase the usage and importance of refs, in such state access need or problem!

It's simple! We can always make a custom hook! Use refs! And update the state in ref! Or a callback that hold the new state! Depending on usage! We set the ref on the render (directly in the custom hook [the block execute in render()])! Or in a useEffect()! That re-run at each render or depending on the dependencies!

Note about useEffect() and refs setting

To note about useEffect()

useEffect => useEffect runs asynchronously and after a render is painted to the screen.

  • You cause a render somehow (change state, or the parent re-renders)
  • React renders your component (calls it)
  • The screen is visually updated
  • THEN useEffect runs

Very important of a thing! useEffect() run after render() finish and the screen is visually updated! It run last! You should be aware!

Generally however! Effects should be run on useEffect()! And so any custom hook will be ok! As it's useEffect() will run after painting and before any other in render useEffect()! If not! As like needing to run something in the render directly! Then you should just pass the state directly! Some people may pass a callback! Imagine some Logic component! And a getState callback passed to it! Not a good practice!
And if you do something somewhere of some of such sense! And talking about ref! Make sure the refs are updated right! And before!

But generally you'll never have a problem! If you do then it's a smell! the way you are trying to go with is high probably not the right good way!

In the whole i hope that gave you a good sense!

score:7

You're creating a closure because gameStart() "captures" the value of gamePlayTime once when the useEffect hook runs and never updates after that.

To get around this, you must use the functional update pattern of React hook state updating. Instead of passing a new value directly to setGamePlayTime(), you pass it a function and that function receives the old state value when it executes and returns a new value to update with. e.g.:

setGamePlayTime((oldValue) => {
  const someNewValue = oldValue + 1;
  return someNewValue;
});

Try this (essentially just wrapping the contents of your setInterval function with a functional state update):

const [gamePlayTime, setGamePlayTime] = React.useState(100);
let targetShowTime = 3;

// call function
React.useEffect(() => {
    gameStart();
  }, []);

const gameStart = () => {
    gameStartInternal = setInterval(() => {
      setGamePlayTime((oldGamePlayTime) => {
        console.log(oldGamePlayTime); // will print previous gamePlayTime value
        if (oldGamePlayTime % targetShowTime === 0) {
          const random = (Math.floor(Math.random() * 10000) % wp("70")) + wp("10");
          const targetPosition = { x: random, y: hp("90") };
          const spinInfoData = getspinArray()[Math.floor(Math.random() * 10) % 4];
          NewSpinShow(targetPosition, spinInfoData, spinSpeed);
        }
        return oldGamePlayTime - 1;
      });
    }, 1000);
  };

Related Query

More Query from same tag