3

I'm trying to get useState to work when a user clicks up or down arrows and for some reason the state is not been updated.

Even if I setTimeout to see if index gets updated still not. Totally confused here with this.

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

const Wrapper = ({ children }) => {
  const [index, setIndex] = useState(-1);

  useEffect(() => {
    document.addEventListener('keydown', handler);
    return () => {
      document.removeEventListener('keydown', handler);
    };
  }, []);

  const handler = (event) => {
    if (event.key === 'ArrowDown') {
      setIndex(index + 1);
      console.log('index = ', index);
    }
  };

  return (
    <div>
      {children}
    </div>
  );
};

export default Wrapper;
skyboyer
  • 22,209
  • 7
  • 57
  • 64
me-me
  • 5,139
  • 13
  • 50
  • 91
  • 4
    State updates are asynchronous. Your console log will always give you the old state. – trixn Jun 26 '19 at 17:33
  • Possible duplicate of [Why is setState in reactjs Async instead of Sync?](https://stackoverflow.com/questions/36085726/why-is-setstate-in-reactjs-async-instead-of-sync) – John Ruddell Jun 26 '19 at 17:42
  • You should move `handler` inside `useEffect` – Vencovsky Jun 26 '19 at 17:43
  • @JohnRuddell not related to async. would be same problem even if render was sync, but the closure of `handler` came from the 1st render only due to 2nd argument of `useEffect` – Aprillion Jun 26 '19 at 17:44
  • @Vencovsky wouldn't make it work because `useEffect` is executed only during first render due to `[]` argument. But useful advice for performance optimization of already working solutions and to make https://reactjs.org/docs/hooks-rules.html#eslint-plugin happy – Aprillion Jun 26 '19 at 17:46
  • regardless the handler function is logging index before the async process has finished so the OP doesn't see the updated value no? – John Ruddell Jun 26 '19 at 17:47
  • 1
    ok, there are multiple problems: the `index` constant won't be updated within 1 render because of async nature of `setState` (affecting only the `console.log`) + the fact that `index` is a closure from first render due to executing `useEffect` only during the first render (affecting why `index` will be at most 0 in any non-first render, because `-1 + 1` is 0) – Aprillion Jun 26 '19 at 17:50
  • yes, that is true :) i was addressing the async nature of the console log issue :) good points though! – John Ruddell Jun 26 '19 at 18:04

2 Answers2

5

Because of the async nature of setState you should log the previous state.

setIndex(prev => {
  console.log("prev = ", prev);
  return prev + 1;
});

Edit Q-56778259-LogPrevState

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
0

The hint is in const [index... - it will never change. Use functional form of setState:

const handler = (event) => {
  if (event.key === 'ArrowDown') {
    setIndex(oldIndex => oldIndex + 1)
  }
}

It's because the index during the first render will be -1, never change even in the closure inside the handle function. During a new render, a new handle function with different constant index will be created, but that function will be ignored because of useEffect(fn, []) second argument ensures that fn will be executed ONLY during first render.

See https://overreacted.io/a-complete-guide-to-useeffect/ for more details.

Aprillion
  • 21,510
  • 5
  • 55
  • 89
  • Is there anyway to get the value of current outside of the useEffect closure ? – me-me Jun 26 '19 at 20:42
  • a closure can be imagined as a bill from supermarket, if one day you buy a milk, next day an orange, what would be on the bill from first day? by default, everything is lost from previous render, only the current props and state are available, and only by explicitly saving the old value it can be accessed (in your case, the handle function from first render passed to addEventListener) – Aprillion Jun 26 '19 at 21:38
  • Right but I thought I was saving the value in index ? How would I save anything in the handle function ? Sorry not getting what you are saying. – me-me Jun 27 '19 at 15:56
  • In the sense I tried to explain, the value of `index` *was "saved"* inside the `handler` function... but both are `const`-ants created inside another function (`Wrapper`), so each time the `Wrapper` function is executed (== in each render), they are completely different constants with independent values. Feel free to ask a more complete question as a separate stackoverflow question please, I do not understand what you are trying to ask in the short comment :( – Aprillion Jun 28 '19 at 20:04