1

I'm trying to use new React hooks capabilities, but stumbled a bit.

Fiddle

I have a useEffect which calls setInterval, which updates local state. Like this:

  const [counter, setCounter] = React.useState(0);

  React.useEffect(() => {
    const k = setInterval(() => {
        setCounter(counter + 1);
    }, 1000);
    return () => clearInterval(k);
  }, []);

  return (
    <div>Counter via state: {counter}<br/></div>
  );

It doesn't work right, because counter is captured on first call and so counter is stuck at 1 value.

If i use refs, ref is updated, but rerender is not called (will only see 0 value in UI):

  const counterRef = React.useRef(0);

  React.useEffect(() => {
    const k = setInterval(() => {
      counterRef.current += 1;
    }, 1000);
    return () => clearInterval(k);
  }, []);

  return (
    <div>Counter via ref: {counterRef.current}</div>
  );

I can make what i want by combining them, but it really doesn't look right:

  const [counter, setCounter] = React.useState(0);
  const counterRef = React.useRef(0);

  React.useEffect(() => {
    const k = setInterval(() => {
        setCounter(counterRef.current + 1);
      counterRef.current += 1;
    }, 1000);
    return () => clearInterval(k);
  }, []);

  return (
    <div>Counter via both: {counter}</div>
  );

Could you please tell who to handle such cases properly with hooks?

nidu
  • 549
  • 2
  • 18
  • @estus yeah, turns out it's duplicate. My initial problem doesn't contain setInterval and here i added it just to make a simple example, but eventually problem is the same. – nidu Jan 28 '19 at 08:45

2 Answers2

1

useRef is useful only in cases when component update is not desirable. But the problem with accessing current state in asynchronous useEffect can be fixed with same recipe, i.e. using object reference instead of immutable state:

  const [state, setState] = React.useState({ counter: 0 });

  React.useEffect(() => {
    const k = setInterval(() => {
        state.counter++;
        setCounter(state);
    }, 1000);
    return () => clearInterval(k);
  }, []);

The use of mutable state is discouraged in React community because it has more downsides than immutable state.

As with setState in class components, state updater function can be used to get current state during state update:

  const [counter, setCounter] = React.useState(0);

  React.useEffect(() => {
    const k = setInterval(() => {
        setCounter(conter => counter + 1);
    }, 1000);
    return () => clearInterval(k);
  }, []);

Alternatively, setTimeout can be used to set new interval every second:

  React.useEffect(() => {
    const k = setTimeout(() => {
        setCounter(counter + 1);
    }, 1000);
    return () => clearInterval(k);
  }, [counter]);
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
1

I have found myself in this situation a couple times by now, and my prefered solution is to use useReducer instead of useState, like so:

  const [counter, dispatch] = React.useReducer((state = 0, action) => {
    // better declared outside of the component
    if (action.type === 'add') return state + 1
    return state
  });

  React.useEffect(() => {
    const k = setInterval(() => {
        dispatch({ type: 'add' });
    }, 1000);
    return () => clearInterval(k);
  }, []);

  return (
    <div>Counter via state: {counter}<br/></div>
  );

Although is adds a bit of boilerplate, it really simplifies the "when is my variable taken in account ?". More here https://reactjs.org/docs/hooks-reference.html#usereducer

Bear-Foot
  • 744
  • 4
  • 12
  • The solution is very neat! It's honestly hard to decide which answer is more spot on. It may become more unwieldy however if i need to update multiple states there, but i guess with `setState(function)` it would be pretty much the same. – nidu Jan 28 '19 at 08:48
  • @nidu IIRC, `useState` uses `useReducer` internally, the former is no-nonsense way to update state, while the latter is Redux-like. Pick one that suits your style. – Estus Flask Jan 28 '19 at 09:27