Search code examples
reactjs

React Hotkeys does not have access to component's state


My goal is to perform an action when keyboard button is pressed, using current value of variable that I declared with useState.

I reproduced my problem in code example below:

import React from 'react';
import { HotKeys } from 'react-hotkeys';
import { useCallback, useEffect, useMemo, useState } from 'react';

const keyMap = {
    MOVE_DOWN: 'a'
};

export function App(props) {
  const [test, setTest] = useState(0);

  const focusNextRow = useCallback(() => {
      console.log("test", test);
      setTest(test => test + 1);
      console.log("test2", test);
  }, [test]);

  useEffect(() => {
      console.log("TEST IS NOW ", test);
  }, [test]);

  const handlers = {
      MOVE_DOWN: focusNextRow
  };

  return (
    <div className='App'>
      <HotKeys keyMap={keyMap} handlers={handlers}>
        <div>test</div>
      </HotKeys>
    </div>
  );
}

Result looks like this:

enter image description here

As you can see: test value in component is updated, but method that is called on keyboard press has always default value of this state (which is 0). How can I fix that?


Solution

  • It appears that the HotKeys component does a bit of configuration memoization where it doesn't "listen" to updates to certain props.

    You can use the allowChanges prop to let the HotKeys component react to the handlers changes/updates. See Component Props API for details.

    /**
     * Whether the keyMap or handlers are permitted to change after the
     * component mounts. If false, changes to the keyMap and handlers
     * props will be ignored
     *
     * Optional.
     */
    allowChanges={false}
    
    import { useCallback, useEffect, useMemo, useState } from "react";
    import { HotKeys } from "react-hotkeys";
    import "./styles.css";
    
    const keyMap = {
      MOVE_DOWN: "a",
    };
    
    export default function App() {
      const [test, setTest] = useState(0);
    
      const focusNextRow = useCallback(() => {
        console.log("test", test);
        setTest((test) => test + 1);
        console.log("test2", test);
      }, [test]);
    
      useEffect(() => {
        console.log("TEST IS NOW ", test);
      }, [test]);
    
      const handlers = {
        MOVE_DOWN: focusNextRow,
      };
    
      return (
        <div className="App">
          <HotKeys keyMap={keyMap} allowChanges handlers={handlers}>
            <div>test</div>
          </HotKeys>
        </div>
      );
    }
    

    That said, this still leaves a basic stale Javascript closure problem where you are attempting to log the React state after enqueueing a state update. React state updates are processed asynchronously, so you can't ever log the state immediately after it's enqueued anyway. See The useState set method is not reflecting a change immediately for full details.

    The useEffect hook is the correct method for logging state updates.

    If you wanted, you could log the "before" and "after" values in the state updater function

    const focusNextRow = useCallback(() => {
      setTest((test) => {
        console.log("test", test);
        const nextTest = test + 1;
        console.log("test2", nextTest);
        return nextTest;
      });
    }, []);
    

    but since the state updater functions are to be considered pure functions this should generally be avoided. But it could be useful/handy as a debugging step.