Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to register event with useEffect hooks?

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?

like image 936
Isaac Avatar asked Apr 08 '19 02:04

Isaac


People also ask

How does useEffect () hook gets executed?

useEffect() hook runs the side-effect after initial rendering, and on later renderings only if the name value changes.

Can I use useEffect in a custom hook?

What is the React UseEffect? The useEffect hook has superpowers that enable us to design our custom hooks. When a change occurs, it allows us to perform side effects in functional components. It allows data retrieval, DOM modification, and function execution each time a component renders.

Can I set state inside a useEffect hook?

It's ok to use setState in useEffect you just need to have attention as described already to not create a loop. The reason why this happen in this example it's because both useEffects run in the same react cycle when you change both prop.


2 Answers

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>   ); 
like image 67
Shubham Khatri Avatar answered Sep 22 '22 07:09

Shubham Khatri


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

There are some 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 states

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/[email protected]/umd/react.production.min.js"></script> <script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef } = React</script>

2. 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/[email protected]/umd/react.production.min.js"></script> <script src="https://unpkg.com/[email protected]/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/[email protected]/umd/react.production.min.js"></script> <script src="https://unpkg.com/[email protected]/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

3. useRef / store callback in mutable ref

const cbRef = useRef(handleUserKeyPress); useEffect(() => { cbRef.current = handleUserKeyPress; }); // update after 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/[email protected]/umd/react.production.min.js"></script> <script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>

✔ for callbacks/event handlers not used for re-render data flow
✔ no need to manage dependencies
✖ only recommended as last option by React docs
✖ more imperative approach

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

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/[email protected]/umd/react.production.min.js"></script> <script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>

For completness, here are some key points on useCallback in general:

✔ all-purpose pragmatic solution
✔ minimal invasive
✖ manual dependencies' management
useCallback makes function definition more verbose/cluttered

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/[email protected]/umd/react.production.min.js"></script> <script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef } = React</script>
like image 21
ford04 Avatar answered Sep 20 '22 07:09

ford04