Artur Carvalho
Artur Carvalho

Reputation: 7167

Varying number of hooks for keyboard shortcuts

I have a react component that has multiple choices. I've associated to each choice a shortcut. For that I'm using "react-hotkeys-hook".

So answer first answer has shortcut 1, second answer has answer 2 and so on.

The thing is, after answering, I update the view to show another question that can have a different number of choices. This makes the hooks blow up because the number of hooks varied from render to render.

// sort of pseudo-code

const comp = () => {
  for(let i=1; i < 6; i++) {
  useHotKey(i, callback, [dependencies])
  }

  return (
    <>
     <choice/>
     <choice/>
    </>
  )
}

How would I go about implementing this? Currently I just create 5 shortcuts by default, but I'd like the number of keyboard shortcuts to not be hard-coded.

Upvotes: 1

Views: 1000

Answers (2)

HMR
HMR

Reputation: 39270

Maybe the following example can help you out:

const useHotKey = (keys, callback) => {
  React.useEffect(() => {
    const keyUp = (e) => {
      if (keys.includes(e.key)) {
        callback(e.key);
      }
    };
    document.body.addEventListener('keyup', keyUp);
    return () =>
      document.body.removeEventListener('keyup', keyUp);
  }, [callback, keys]); //callback never chanes but keys does
};
const questions = [
  ['1', '2', '3', '4'],
  ['1', '2'],
  ['1', '2', '3'],
  ['a', 'b', 'c'],
];
const Answer = ({ answers, answer }) => {
  useHotKey(answers, answer);
  return <pre>ansers:{JSON.stringify(answers)}</pre>;
};
const App = () => {
  const [current, setCurrent] = React.useState(0);
  const [answers, setAnswers] = React.useState([]);
  //answer is the callback but never changes because
  //  there are no dependencies
  const answer = React.useCallback((answer) => {
    setAnswers((answers) => [...answers, answer]);
    setCurrent((current) => current + 1);
  }, []); //no dependencies, answers only created on mount
  return current < questions.length ? (
    <Answer answers={questions[current]} answer={answer} />
  ) : (
    <pre>
      answers given:{JSON.stringify(answers, undefined, 2)}
    </pre>
  );
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Upvotes: 3

Dennis Vash
Dennis Vash

Reputation: 53894

You can't have a dynamic call of hooks due to Rules of Hooks, the better approach is using composition.

Your Choice component should have a hotKey and callback props, the parent should dictate the number of choices.

const Choice = ({ hotKey, callback }) => {
  useHotKey(hotKey, callback);
  return <>...</>;
};

// Usages
<Choice hotKey="1" callback={()=>console.log('1')} />
choices.map((_,index) => <Choice key={index} hotKey={index} ... />);

See Thinking in React, and HMR's code example.

Upvotes: 3

Related Questions