React: window event subscription

Viewed 79

I am getting into React and trying to create some kind of keyboard trainer app.

I want to add a keypress event listener to the window object to get the letters, and then update the state I created with useState() hook.

So I add the event listener inside useEffect hook. with the custom handler. But the setPhrase function doesn't seem to work well in this case.

Expected result: After each correct key press the phrase.written to add this key as text, phrase.left to substring by this letter;

Actual result: State is renewed every time, so the state doesn't update after setPhrase call

The code:

export default () => {
  const [initialPhrase] = useState("Test Phrase");
  const [phrase, setPhrase] = useState({
    left: initialPhrase,
    written: "",
  });

  const handleKeyPress = (event) => {
    const requiredLetter = phrase.left.charAt(0);

    if (requiredLetter === event.key) {
      setPhrase({
        written: phrase.written + event.key,
        left: phrase.left.substring(1, phrase.left.length),
      });
    }
    console.debug(phrase, requiredLetter, event.key, phrase);
  };

  useEffect(() => {
    window.addEventListener("keypress", handleKeyPress);
    return () => window.removeEventListener("keypress", handleKeyPress);
  }, []);

  return (
    <div>
      <p className="phrase">
        <span className="phrase--part phrase--part__written">
          {phrase.written}
        </span>
        <span className="phrase--part phrase--part__left">{phrase.left}</span>
      </p>
    </div>
  );
};

Playground: https://stackblitz.com/edit/react-qgbjaf?file=src/index.js

Note: if you place this handler to any element inside component, it is going to work just as expected

2 Answers

in your event handler method you cant reach the updated state, because state variables don't update itself inside an EventListener. just use useRef like this:

const phraseRef = useRef({
    left: initialPhrase,
    written: '',
  })

  const handleKeyPress = (event) => {
    const requiredLetter = phraseRef.current.left.charAt(0);

    if (requiredLetter === event.key) {
      setPhrase({
        written: phraseRef.current.written + event.key,
        left: phraseRef.current.left.substring(1, phraseRef.current.left.length),
      });
    
      phraseRef.current = {
        written: phraseRef.current.written + event.key,
        left: phraseRef.current.left.substring(1, phrase.left.length),
      }
    }
    console.debug(phrase, requiredLetter, event.key, phrase);
  };

The problem is that the handleKeyPress Method is not updated after the phrase state updated. Therefore, you need to add the handleKeyPress method to the useEffect dependency. Now after each phrase state update, the method gets the correct state of phrases

Stackblitz fork: https://stackblitz.com/edit/react-bx5qp7?file=src/index.js

import React, { useState, useEffect, useCallback } from 'react';
import ReactDOM from 'react-dom';
import './style.css';

const KeyboardTrainer = () => {
  const [initialPhrase] = useState('Test Phrase');
  const [phrase, setPhrase] = useState({
    left: initialPhrase,
    written: '',
  });

  const handleKeyPress = (event) => {
      const requiredLetter = phrase.left.charAt(0);
      if (requiredLetter === event.key) {
        setPhrase({
          written: phrase.written + event.key,
          left: phrase.left.substring(1, phrase.left.length),
        });
      }
    }

  useEffect(() => {
    window.addEventListener('keypress', (e) => {
      handleKeyPress(e);
    });
    return () => window.removeEventListener("keypress", handleKeyPress);
  }, [handleKeyPress]);

  return (
    <div>
      <p className="phrase">
        <span className="phrase--part phrase--part__written">
          {phrase.written}
        </span>
        <span className="phrase--part phrase--part__left">{phrase.left} 
        </span> 
      </p>
    </div>
     )};

ReactDOM.render(<KeyboardTrainer />, document.getElementById('root'));
Related