152

I am following a Udemy course on how to register events with hooks, the instructor gave the below code:

  const [userText, setUserText] = useState('');

  const handleUserKeyPress = event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(`${userText}${key}`);
    }
  };

  useEffect(() => {
    window.addEventListener('keydown', handleUserKeyPress);

    return () => {
      window.removeEventListener('keydown', handleUserKeyPress);
    };
  });

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );

Now it works great but I'm not convinced that this is the right way. The reason is, if I understand correctly, on each and every re-render, events will keep registering and deregistering every time and I simply don't think it is the right way to go about it.

So I made a slight modification to the useEffect hooks to below

useEffect(() => {
  window.addEventListener('keydown', handleUserKeyPress);

  return () => {
    window.removeEventListener('keydown', handleUserKeyPress);
  };
}, []);

By having an empty array as the second argument, letting the component to only run the effect once, imitating componentDidMount. And when I try out the result, it's weird that on every key I type, instead of appending, it's overwritten instead.

I was expecting setUserText(${userText}${key}); to have new typed key append to current state and set as a new state but instead, it's forgetting the old state and rewriting with the new state.

Was it really the correct way that we should register and deregister event on every re-render?

Isaac
  • 12,042
  • 16
  • 52
  • 116

9 Answers9

176

The best way to go about such scenarios is to see what you are doing in the event handler.

If you are simply setting state using previous state, it's best to use the callback pattern and register the event listeners only on initial mount.

If you do not use the callback pattern, the listeners reference along with its lexical scope is being used by the event listener but a new function is created with updated closure on each render; hence in the handler you will not be able to access the updated state

const [userText, setUserText] = useState("");
const handleUserKeyPress = useCallback(event => {
    const { key, keyCode } = event;
    if(keyCode === 32 || (keyCode >= 65 && keyCode <= 90)){
        setUserText(prevUserText => `${prevUserText}${key}`);
    }
}, []);

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

  return (
      <div>
          <h1>Feel free to type!</h1>
          <blockquote>{userText}</blockquote>
      </div>
  );
GROVER.
  • 4,071
  • 2
  • 19
  • 66
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • 2
    but this will create a new bound function every keypress. if your focus is perfromance a local state variable is much better – di3 Apr 08 '19 at 08:51
  • 1
    @di3 If you use `useCallback` to define the function with empty dependency array, you won't have that issue as well – Shubham Khatri Apr 08 '19 at 10:00
  • if `handleUserKeyPress` isn't pulled inside `useEffect`, then it's an external dependency and should be explicit: `useEffect(..., [handleUserKeyPress])` – Aprillion Apr 08 '19 at 11:51
  • @Aprillion thats not true, empty array is specially used for mount/unmount check. the intention is to call it only once. – di3 Apr 08 '19 at 14:04
  • if the intention is to call it once, then don't create a new unused function in every render and pull it inside useEffect. your solution would create a bug if you add dependencies to 2nd argument of useCallback because the useEffect is missing its own dependency – Aprillion Apr 08 '19 at 14:27
  • 11
    I see the important is to use `prevUserText ` to reference the previous state of `userText`. what if I need to access multiple states? How can I get access to all of the previous state? – Joey Yi Zhao Jun 04 '19 at 11:48
  • 1
    How can we get previous state if we're using `useReducer` instead of `useState` ? – Eesa Oct 08 '19 at 09:01
  • this is able to change state using the previous state, but how would you be able to access the current state? – Jeremy Nelson Dec 05 '19 at 02:26
  • @JeremyNelson, if by accessing current state, you mean accessing the updated state, you can either do it using a new useEffect or since you already have the value to be returned from within setUserText, you can use it before returning from its callback – Shubham Khatri Dec 05 '19 at 05:41
  • @ShubhamKhatri this is exactly what i was looking for! <3 Can somebody proficient with hooks explain in a few words when to pass a parameter to the effect itself ( the first arg of useEffect) and when not to? It seems to work fine in both cases when i log a state update in a didupdate-manner, but i'm not clear on whether this goes for more complex cases. – rtviii Jan 09 '20 at 02:51
  • 1
    @tractatusviii I think this post will help you: https://stackoverflow.com/questions/55840294/how-to-fix-missing-dependency-warning-when-using-useeffect-react-hook/55854902#55854902 – Shubham Khatri Jan 09 '20 at 03:27
  • @Eesa You always have the previous state available to you in the reducer function supplied to `useReducer`. The `prevState` callback function is a workaround for `useState`. – izolate Jun 19 '20 at 17:56
  • I had the same problem and I had been struggling with it for a long time. but your solution saved my day :) – Morteza Rahmani Jan 25 '22 at 12:15
  • If I have one more state for example: const [visible, setVisible] = useState(false); and I want to use it in handleUserKeyPress I need to add "visible" to useCallback's dependencies array: [visible]. But in this case keydown listeners will be resubscribed on every change of "visible". Can we avoid it? – Dron007 May 05 '22 at 00:40
74

Issue

[...] on each and every re-render, events will keep registering and deregistering every time and I simply don't think it is the right way to go about it.

You are right. It doesn't make sense to restart event handling inside useEffect on every render.

[...] empty array as the second argument, letting the component to only run the effect once [...] it's weird that on every key I type, instead of appending, it's overwritten instead.

This is an issue with stale closure values.

Reason: Used functions inside useEffect should be part of the dependencies. You set nothing as dependency ([]), but still call handleUserKeyPress, which itself reads userText state.

Solutions

0. useEffectEvent (beta)

Update: React developers proposed an RFC including new useEvent Hook (name and functionality have changed slightly since) to solve this exact type of event-related problem with dependencies.

Until then, there are alternatives depending on your use case:

1. State updater function

setUserText(prev => `${prev}${key}`);

✔ least invasive approach
✖ only access to own previous state, not other state Hooks

const App = () => {
  const [userText, setUserText] = useState("");

  useEffect(() => {
    const handleUserKeyPress = event => {
      const { key, keyCode } = event;

      if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
        setUserText(prev => `${prev}${key}`); // use updater function here
      }
    };

    window.addEventListener("keydown", handleUserKeyPress);
    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, []); // still no dependencies

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>

2. useRef / mutable refs

const cbRef = useRef(handleUserKeyPress);
useEffect(() => { cbRef.current = handleUserKeyPress; }); // update each render
useEffect(() => {
    const cb = e => cbRef.current(e); // then use most recent cb value
    window.addEventListener("keydown", cb);
    return () => { window.removeEventListener("keydown", cb) };
}, []);

const App = () => {
  const [userText, setUserText] = useState("");

  const handleUserKeyPress = event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(`${userText}${key}`);
    }
  };

  const cbRef = useRef(handleUserKeyPress);

  useEffect(() => {
    cbRef.current = handleUserKeyPress;
  });

  useEffect(() => {
    const cb = e => cbRef.current(e);
    window.addEventListener("keydown", cb);

    return () => {
      window.removeEventListener("keydown", cb);
    };
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>

✔ can be used for callbacks/event handlers that shall not trigger re-renders via data flow
✔ no need to manage dependencies
✖ more imperative approach
✖ only recommended as last option by React docs

Take a look at these links for further info: 1 2 3

3. useReducer - "cheat mode"

We can switch to useReducer and have access to current state/props - with similar API to useState.

Variant 2a: logic inside reducer function

const [userText, handleUserKeyPress] = useReducer((state, event) => {
    const { key, keyCode } = event;
    // isUpperCase is always the most recent state (no stale closure value)
    return `${state}${isUpperCase ? key.toUpperCase() : key}`;  
}, "");

const App = () => {
  const [isUpperCase, setUpperCase] = useState(false);
  const [userText, handleUserKeyPress] = useReducer((state, event) => {
    const { key, keyCode } = event;
    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      // isUpperCase is always the most recent state (no stale closure)
      return `${state}${isUpperCase ? key.toUpperCase() : key}`;
    }
  }, "");

  useEffect(() => {
    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
      <button style={{ width: "150px" }} onClick={() => setUpperCase(b => !b)}>
        {isUpperCase ? "Disable" : "Enable"} Upper Case
      </button>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>

Variant 2b: logic outside reducer function - similar to useState updater function

const [userText, setUserText] = useReducer((state, action) =>
      typeof action === "function" ? action(state, isUpperCase) : action, "");
// ...
setUserText((prevState, isUpper) => `${prevState}${isUpper ? 
  key.toUpperCase() : key}`);

const App = () => {
  const [isUpperCase, setUpperCase] = useState(false);
  const [userText, setUserText] = useReducer(
    (state, action) =>
      typeof action === "function" ? action(state, isUpperCase) : action,
    ""
  );

  useEffect(() => {
    const handleUserKeyPress = event => {
      const { key, keyCode } = event;
      if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
        setUserText(
          (prevState, isUpper) =>
            `${prevState}${isUpper ? key.toUpperCase() : key}`
        );
      }
    };

    window.addEventListener("keydown", handleUserKeyPress);
    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
      <button style={{ width: "150px" }} onClick={() => setUpperCase(b => !b)}>
        {isUpperCase ? "Disable" : "Enable"} Upper Case
      </button>
    </div>
  );
}


ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>

✔ no need to manage dependencies
✔ access multiple states and props
✔ same API as useState
✔ extendable to more complex cases/reducers
✖ slightly less performance due to inline reducer (kinda neglectable)
✖ slightly increased complexity of reducer

Inappropriate solutions

useCallback

While it can be applied in various ways, useCallback is not suitable for this particular question case.

Reason: Due to the added dependencies - userText here -, the event listener will be re-started on every key press, in best case being not performant, or worse causing inconsistencies.

const App = () => {
  const [userText, setUserText] = useState("");

  const handleUserKeyPress = useCallback(
    event => {
      const { key, keyCode } = event;

      if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
        setUserText(`${userText}${key}`);
      }
    },
    [userText]
  );

  useEffect(() => {
    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, [handleUserKeyPress]); // we rely directly on handler, indirectly on userText

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>

Declare handler function inside useEffect

Declaring the event handler function directly inside useEffect has more or less the same issues as useCallback, latter just causes a bit more indirection of dependencies.

In other words: Instead of adding an additional layer of dependencies via useCallback, we put the function directly inside useEffect - but all the dependencies still need to be set, causing frequent handler changes.

In fact, if you move handleUserKeyPress inside useEffect, ESLint exhaustive deps rule will tell you, what exact canonical dependencies are missing (userText), if not specified.

const App =() => {
  const [userText, setUserText] = useState("");

  useEffect(() => {
    const handleUserKeyPress = event => {
      const { key, keyCode } = event;

      if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
        setUserText(`${userText}${key}`);
      }
    };

    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, [userText]); // ESLint will yell here, if `userText` is missing

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
ford04
  • 66,267
  • 20
  • 199
  • 171
18

new answer:

useEffect(() => {
  function handlekeydownEvent(event) {
    const { key, keyCode } = event;
    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(prevUserText => `${prevUserText}${key}`);
    }
  }

  document.addEventListener('keyup', handlekeydownEvent)
  return () => {
    document.removeEventListener('keyup', handlekeydownEvent)
  }
}, [])

when using setUserText, pass the function as the argument instead of the object, the prevUserText will be always the newest state.


old answer:

try this, it works same as your original code:

useEffect(() => {
  function handlekeydownEvent(event) {
    const { key, keyCode } = event;
    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(`${userText}${key}`);
    }
  }

  document.addEventListener('keyup', handlekeydownEvent)
  return () => {
    document.removeEventListener('keyup', handlekeydownEvent)
  }
}, [userText])

because in your useEffect() method, it depends on the userText variable but you don't put it inside the second argument, else the userText will always be bound to the initial value '' with argument [].

you don't need to do like this, just want to let you know why your second solution doesn't work.

Spark.Bao
  • 5,573
  • 2
  • 31
  • 36
  • By adding `[userText]` is exactly the same as without second argument, right? Reason is I only have `userText` in the above example, and without second argument simply means re-rerender on every props/state changes, I don't see how it answer my question. **P/S: ** I'm not the downvoter, thanks for your answer anyway – Isaac Apr 08 '19 at 02:53
  • hey @Isaac , yep, it is same as without second argument, I just want to let you know why your second solution doesn't work, because your second solution `useEffect()` depend on the `userText` variable but you didn't put inside the second arguments. – Spark.Bao Apr 08 '19 at 03:08
  • But by adding in `[userText]`, it also means register and deregister the event on every re-render right? – Isaac Apr 08 '19 at 03:14
  • exactly! that why I say it is same with your first solution. – Spark.Bao Apr 08 '19 at 03:17
  • haha yeap I get the idea, but point is, I'm feeling it's inefficient, because I feel an event shouldn't be `register` and `deregister` on every re-render, but instead should be like the classic `componentDidMount` and `componentWillUnmount`, where they only register once, if you get my point? – Isaac Apr 08 '19 at 03:19
  • While your current recommendation is having register event at `componentDidUpdate`, which isn't the same way we done things right? – Isaac Apr 08 '19 at 03:20
  • 1
    got what you mean, if you really want to register it only one time in this example, then you need to use `useRef`, just as @Maaz Syed Adeeb 's answer. – Spark.Bao Apr 08 '19 at 03:23
  • I've just upvoted your answer for going thru this with me. Wondering based on your understanding, is `useRef` a more recommended way in comparison with di3's answer above? – Isaac Apr 08 '19 at 03:47
  • @Isaac in my understanding, yes. – Spark.Bao Apr 08 '19 at 05:42
  • hey @Isaac now I have the newer understanding, you don't need `useRef` or `useCallback` neither, just `useState` and `useEffect`, see my new answer. – Spark.Bao Apr 11 '19 at 01:17
3

For your use case, useEffect needs a dependency array to track changes and based on the dependency it can determine whether to re-render or not. It is always advised to pass a dependency array to useEffect. Kindly see the code below:

I have introduced useCallback hook.

const { useCallback, useState, useEffect } = React;

  const [userText, setUserText] = useState("");

  const handleUserKeyPress = useCallback(event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(prevUserText => `${prevUserText}${key}`);
    }
  }, []);

  useEffect(() => {
    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, [handleUserKeyPress]);

  return (
    <div>
      <blockquote>{userText}</blockquote>
    </div>
  );

Edit q98jov5kvq

codejockie
  • 9,020
  • 4
  • 40
  • 46
  • I've tried your solution, but it's exactly the same as `[userText]` or without second argument. Basically we put a `console.log` inside `useEffect`, we will see that the logging is firing every re-render, which also means, `addEventListender` is running every re-render – Isaac Apr 08 '19 at 03:26
  • I want to believe that is an expected behaviour. I updated my answer. – codejockie Apr 08 '19 at 03:39
  • On your sandbox, you've put a statement `console.log('>');` within `useEffect` hooks, and by using your updated code, it's still logging everytime, which also means the events are still registering on every re-render – Isaac Apr 08 '19 at 03:45
  • No, for `addEventListener` it doesn't work that way. It is not called multiple times. https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Multiple_identical_event_listeners – codejockie Apr 08 '19 at 03:48
  • 1
    but because of `return () => {window.removeEventListener('keydown', handleUserKeyPress)}`, on every re-render, the component will register and deregister – Isaac Apr 08 '19 at 04:00
  • We can put another console log inside `return () => { console.log('im removing!')}` and can see that on every keystroke, event will be deregister and re-register – Isaac Apr 08 '19 at 04:01
  • 1
    Exactly the behaviour that I wished for, but you can observe it @ https://codesandbox.io/s/n5j7qy051j – Isaac Apr 08 '19 at 04:06
  • @Isaac it wouldn't re-register on every *render*, only on every change of `userText` (so on most, but not all, keystrokes) – Aprillion Apr 08 '19 at 11:53
  • @Aprillion: I understand your point but dont think this solution is what I'm truly looking for. You may have a look at the accepted answer instead – Isaac Apr 08 '19 at 12:18
1

You'll need a way to keep track of the previous state. useState helps you keep track of the current state only. From the docs, there is a way to access the old state, by using another hook.

const prevRef = useRef();
useEffect(() => {
  prevRef.current = userText;
});

I've updated your example to use this. And it works out.

const { useState, useEffect, useRef } = React;

const App = () => {
  const [userText, setUserText] = useState("");
  const prevRef = useRef();
  useEffect(() => {
    prevRef.current = userText;
  });

  const handleUserKeyPress = event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(`${prevRef.current}${key}`);
    }
  };

  useEffect(() => {
    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
maazadeeb
  • 5,922
  • 2
  • 27
  • 40
1

The accepted answer is working but if you are registering BackHandler event, make sure to return true in your handleBackPress function, e.g:

const handleBackPress= useCallback(() => {
   // do some action and return true or if you do not
   // want the user to go back, return false instead
   return true;
 }, []);

 useEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', handleBackPress);
    return () =>
       BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
  }, [handleBackPress]);
Mahdieh Shavandi
  • 4,906
  • 32
  • 41
0

Here is the useRef solution based on @ford04's answer and moved to custom hook. I like it most because it doesn't require adding any manual dependencies and allows to hide all the complexity in the custom hook.

const useEvent = (eventName, eventHandler) => {
  const cbRef = useRef(eventHandler)

  useEffect(() => {
    cbRef.current = eventHandler
  }) // update after each render

  useEffect(() => {
    console.log("+++ subscribe")
    const cb = (e) => cbRef.current(e) // then use most recent cb value
    window.addEventListener(eventName, cb)
    return () => {
      console.log("--- unsubscribe")
      window.removeEventListener(eventName, cb)
    }
  }, [eventName])
  return
}

Usage in App:

function App() {
  const [isUpperCase, setUpperCase] = useState(false)
  const [userText, setUserText] = useState("")

  const handleUserKeyPress = (event) => {
    const { key, keyCode } = event
    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      const displayKey = isUpperCase ? key.toUpperCase() : key
      const newText = userText + displayKey
      setUserText(newText)
    }
  }
  useEvent("keydown", handleUserKeyPress)

  return (
    <div>
      <h1>Feel free to type!</h1>
      <label>
        <input
          type="checkbox"
          defaultChecked={isUpperCase}
          onChange={() => setUpperCase(!isUpperCase)}
        />
        Upper Case
      </label>
      <blockquote>{userText}</blockquote>
    </div>
  )
}

Edit

Dron007
  • 131
  • 1
  • 9
-1

In the second approach, the useEffect is bound only once and hence the userText never gets updated. One approach would be to maintain a local variable which gets updated along with the userText object on every keypress.

  const [userText, setUserText] = useState('');
  let local_text = userText
  const handleUserKeyPress = event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      local_text = `${userText}${key}`;
      setUserText(local_text);
    }
  };

  useEffect(() => {
    window.addEventListener('keydown', handleUserKeyPress);

    return () => {
      window.removeEventListener('keydown', handleUserKeyPress);
    };
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );

Personally I don't like the solution, feels anti-react and I think the first method is good enough and is designed to be used that way.

varun agarwal
  • 1,491
  • 6
  • 9
  • Do you mind to include some code to demonstrate how to achieve my objective in second method? – Isaac Apr 08 '19 at 02:51
-1

you dont have access to the changed useText state. you can comapre it to the prevState. store the state in a variable e.g.: state like so:

const App = () => {
  const [userText, setUserText] = useState('');

  useEffect(() => {
    let state = ''

    const handleUserKeyPress = event => {
      const { key, keyCode } = event;
      if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
        state += `${key}`
        setUserText(state);
      }   
    };  
    window.addEventListener('keydown', handleUserKeyPress);
    return () => {
      window.removeEventListener('keydown', handleUserKeyPress);
    };  
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );  
};
di3
  • 584
  • 3
  • 10