7. React - hooks Lifecycle of component 1 First render (Mounting) 2 Rerender (Updating) 2 Unmounting 3 Hooks - lifecycle of function components 3 “useState” hook 4 Functional updates 4 Merging state 4 “useEffect” hook 5 Dependency array 5 Cleanup 6 Implementation 7 “useMemo” hook 8 Implementation 8 “useCallback” hook 9 Implementation 9 Custom hook 10 Lifecycle of component So let’s have a closer look at the main job of React (but also other js frameworks), in which point React component comes into existence, what happens at this point and when the component is removed from DOM. The main job of React is to figure out how to modify the DOM to match what the components want to be rendered on the screen. 1 We created a simple Tic-Tac-Toe game, where we have the following tree of main components (some components are ignored for clarity): ​App -> Board -> Square -> PlayerIcon -> Typography / Cross / Circle​. We also created a condition if we wanted to show a Cross icon, a Circle icon or index inside the button. First render (Mounting) Everything is initialized into default state and rendered as follows. 1. index.tsx​ as a starting point renders App component into ​
​. 2. App renders Board. 3. Board state is initialized with an array of numbers, player state is initialized with “X” and renders 9 Square components with PlayerIcons inside. 4. Each PlayerIcon receives a number, so no icons are rendered at the beginning. At the moment of component being added to DOM, the component is ​mounting​. It needs to be mentioned, because usage in practice is very common, while using ​class components, there are lifecycle methods fired during first render in this order (some of them are missing, only needed in advanced usage, out of scope of this course): - constructor​ - initialize stuff like state - render​ - only required method in class component - componentDidMount​ - invoked only once right after first render Since we are focusing on function components only, these methods are not available to us, and the concepts are slightly different, which we’ll come back to later. Rerender (Updating) So what happens after we click on a Square with number 5 on it? 2 1. Square 5 invokes the onSquareClicked(5) handler passed to it’s onClick callback which updates boardValues and also switches player to “O”. 2. Now since the state of the Board component has changed, it needs to be rerendered. 3. Code inside its body is run again and all its children are also rerendered. 4. Square 5 now receives different children and therefore renders a Cross icon. In class component the lifecycle methods are invoked in the order: - render​ - invoked with new state, props - componentDidUpdate​ - method invoked after render, but careful Icarus, there is danger of circular rerendering Unmounting And what happens to component Typography, when it is replaced with a Cross icon? Simply, it’s destroyed, removed, deleted… There is one lifecycle method invoked before component is unmounted: - componentWillUnmount​ - use it when you want to clean something after yourself Hooks - lifecycle of function components Hooks are a new concept in React world that lets you use state and other lifecycle related features without using class components. Hooks are plain JavaScript functions that need to follow few important rules: 1. They need to always be called each rerender​, they can’t be called inside loops, conditions or nested functions. 3 const​ ​Board​: ​FC​<​Props​> = ({ ​isLoggedIn​ }) ​=>​ { ​if​ (!​isLoggedIn​) { ​return​ ​<​div​>​Oooops​​; } ​// ERROR: If isLoggedIn is false, state hooks below wouldn't be called ​// State ​const​ [​player​, ​setPlayer​] = ​useState​<​Player​>(​'X'​); ​const​ [​board​, ​setBoard​] = ​useState​<​BoardState​>({}); // ... 2. Must be used by Function Components or other hooks​, don’t call them from regular JS functions. To properly function they need to be called by React render loop. “useState” hook During the first render - ​mounting phase​, the ​state​ value is taken from the value of initialState argument. The ​setState​ is a function used to replace ​state​ with a new value which triggers a rerender of the component. During subsequent rerenders - ​updating phase​, ​state​ value stays the same and initialState​ is no longer used. Functional updates Setters also accept a function that receives the previous state in its first argument. Example from our Tic Tac Toe assignment. Both examples usually give the same result, so the question is, why would I want to use the longer one? Well imagine a situation, where you want to call setPlayer from a child component. With the first approach, you need to pass both the value and the setter to child the component. With the second approach, there is no need to pass player prop because setPlayer has the information about the previous player, this is a way to avoid unnecessary prop drilling. Merging state Important information about the state setting is that setState function (unlike with class component state) does not automatically merge update objects. That’s why we need to set new board values in our Tic Tac Toe assignment this way. 4 const​ [​state​, ​setState​] = ​useState​(​initialState​); setPlayer​(​player​ === ​'O'​ ? ​'X'​ : ​'O'​); setPlayer​(​p​ ​=>​ (​p​ === ​'O'​ ? ​'X'​ : ​'O'​)); setBoard​(​b​ ​=>​ ({ ...​b​, ​[index]:​ ​player​ })); “useEffect” hook This hook lets you run code in function components only when necessary such as performing side effects like data fetching, setting up a subscription and so on. You can think of this hook as componentDidMount, componentDidUpdate and componentWillUnmount from class components combined. Example usage of hooks can be added into ​Status​ component: By adding this hook, React knows that the Status component needs to do something after render. In this case setup document title. Placing useEffect inside the component lets us access the name prop from the Status function, because the variable is already in a scope. useEffect is by default called right after first render and after every update, in other words useEffect is executed after mounting and also updating. Dependency array So far we would be able to achieve the same goal by simply putting all the code inside the hook into the body of the function. To use this hook properly, we need to provide a dependency array as its second argument. Each rerender, React compares values in this array with the values from previous render and only runs the provided function if these dependencies have changed. In this case, we have a 5 useEffect​(() ​=>​ { ​/* Do stuff */​ }, [​/* Dependencies */​]); const​ ​Status​: ​FC​<​Props​> = ({ ​player​, ​winner​, ​onRestart​ }) ​=>​ { ​const​ ​classes​ = ​useStyles​({ ​winner​ }); ​useEffect​(() ​=>​ { ​document​.​title​ = ​`Player ​${​player​}​'s turn`​; }); // ... useEffect​(() ​=>​ { ​document​.​title​ = ​`Player ​${​player​}​'s turn`​; }, [​player​]); dependency on ​player​ prop of ​Status​ component, which only changes when the ​Board component passes another player to it. Keep in mind that dependencies of useEffect are compared by reference equality, which means that in the example below, variable copy changes every render because each time it is a new array instance even though it’s value is the same. If you want useEffect hook to be called ​exactly once​ at the moment of mounting, pass an empty array​ as a second argument. This is not the same as ​not providing​ a dependencies array since in that case the code runs ​every​ rerender. Cleanup At the moment of unmounting of the component, an optional ​cleanup​ function is called. Common us case for it is for example with event listener functions. 6 const​ ​copy​ = [...​original​]; useEffect​(() ​=>​ { ​console​.​log​(​`Copy changed.`​); }, [​copy​]); useEffect​(() ​=>​ { ​document​.​title​ = ​`Mounting Title FTW`​; }, []); const​ [​scroll​, ​setScroll​] = ​useState​(​window​.​scrollY​); useEffect​(() ​=>​ { ​const​ ​scrollHandler​ = () ​=>​ ​setScroll​(​window​.​scrollY​); ​window​.​addEventListener​(​'scroll'​, ​scrollHandler​); ​return​ () ​=>​ { ​window​.​removeEventListener​(​'scroll'​, ​scrollHandler​); }; }, []); Implementation In our Tic Tac Toe app we are calling the ​setWinner​ setter function inside the onSquareClicked​ handler with the new value of ​getWinner​ every time a square is clicked. We also need to call it inside the ​onBoardRestart​ handler to set it back to undefined. This behavior can be optimized by useEffect. Since board is in the dependency list, this effect will run each time it’s value changes, no matter from what source. 7 const​ ​onSquareClicked​ = (​index​: ​Indexes​) ​=>​ { ​if​ (​board​[​index​]) ​return​; ​const​ ​newBoard​ = { ...​board​, ​[index]:​ ​player​ }; ​setBoard​(​newBoard​); ​setPlayer​((​p​) ​=>​ (​p​ === ​"O"​ ? ​"X"​ : ​"O"​)); ​// Update winner ​setWinner​(​getWinner​(​newBoard​)); }; const​ ​onBoardRestart​ = () ​=>​ { ​setPlayer​(​"X"​); ​setBoard​({}); ​setWinner​(​undefined​); }; const​ ​onSquareClicked​ = (​index​: ​Indexes​) ​=>​ { ​if​ (​board​[​index​]) ​return​; ​setBoard​(​b​ ​=>​ ({ ...​b​, ​[index]:​ ​player​ })); ​setPlayer​((​p​) ​=>​ (​p​ === ​"O"​ ? ​"X"​ : ​"O"​)); }; const​ ​onBoardRestart​ = () ​=>​ { ​setPlayer​(​"X"​); ​setBoard​({}); “useMemo” hook It’s quite similar to useEffect, with one major difference, in that it returns a value. It is used to speed up and to optimise your app by storing the results of expensive function calls by returning the cached result when it’s dependencies don’t change between renders. Inside useMemo, there should not be any side effects running, they should be performed within the useEffect hook instead. Implementation With this hook we can simplify our Board component even further. Since the winner is completely dependent on the current board state and calculating it has no side effects, instead of holding it as state we can just memoize it. 8 }; // Update winner useEffect​(() ​=>​ { ​setWinner​(​getWinner​(​board​)); }, [​board​]); // State const​ [​player​, ​setPlayer​] = ​useState​<​Player​>(​"X"​); const​ [​board​, ​setBoard​] = ​useState​<​BoardState​>({}); // Memoization const​ ​winner​ = ​useMemo​(() ​=>​ ​getWinner​(​board​), [​board​]); // Handlers const​ ​onSquareClicked​ = (​index​: ​Indexes​) ​=>​ { ​if​ (​board​[​index​]) ​return​; ​setBoard​(​b​ ​=>​ ({ ...​b​, ​[index]:​ ​player​ })); ​setPlayer​((​p​) ​=>​ (​p​ === ​"O"​ ? ​"X"​ : ​"O"​)); }; “useCallback” hook Specialized version of useMemo hook, used for memoizing functions. Most commonly it’s used for handlers that are passed as props to other components to prevent unnecessary rerenders. useCallback(fn, deps)​ is equivalent to ​useMemo(() => fn, deps)​. Implementation In our app we have two handlers that are passed to child components, ​onSquareClicked​ and onBoardRestart​. With our new knowledge of useCallback hook we can now attempt to improve these as well. It may seem like we can simply wrap both in useCallback and call it a day, but it’s also important to think about the usage and benefits. After converting ​onBoardRestart to useCallback, we can see that it has no dependencies and it’s also directly passed to ​onRestart prop as a value. This means that now the ​Status component will no longer be unnecessarily re-rendering because it was receiving new reference to our previously non hook callback. So far so good. 9 const​ ​onBoardRestart​ = () ​=>​ { ​setPlayer​(​"X"​); ​setBoard​({}); }; const​ ​onBoardRestart​ = ​useCallback​(() ​=>​ { ​setPlayer​(​'X'​); ​setBoard​({}); }, []); // Usage <​Status​ ​player​=​{​player​}​ ​winner​=​{​winner​}​ ​onRestart​=​{​onBoardRestart​}​ ​/> const​ ​onSquareClicked​ = ​useCallback​((​index​: ​Indexes​) ​=>​ { if​ (​board​[​index​]) ​return​; setBoard​(​b​ ​=>​ ({ ...​b​, ​[index]:​ ​player​ })); Second callback now has a dependency to the ​board state value which is okay. What’s complicating it now is it’s usage. This time it’s not passed as a value but called inside an inline arrow function. Because of this, the ​Square component receives different reference each render anyway. In this case there is ​no benefit from using useCallback so we are better off just keeping the handler as it was before. Custom hook React provides also other types of hooks, it is even possible to create your own ones. Let’s begin with something trivial. 10 setPlayer​(​p​ ​=>​ (​p​ === ​'O'​ ? ​'X'​ : ​'O'​)); }, [​board​]); // Usage <​Square​ ​onClick​=​{​() ​=>​ ​onSquareClicked​(​i​)​}​> const​ ​App​: ​FC​ = () ​=>​ { ​// Number counter ​const​ [​counter​, ​setCounter​] = ​useState​(​0​); ​// Effect to set number into Title ​useEffect​(() ​=>​ { ​document​.​title​ = ​`​${​counter​}​`​; }, [​counter​]); ​// Handler for button onClick method ​const​ ​incrementCounter​ = () ​=>​ ​setCounter​(​p​ ​=>​ ++​p​); ​return​ ( ​<​div​ ​className​=​'App'​> ​<​header​ ​className​=​'App-header'​> ​<​img​ ​src​=​{​logo​}​ ​className​=​'App-logo'​ ​alt​=​'logo'​ ​/> ​<​p​>​{​counter​}​ ​<​button​ ​onClick​=​{​incrementCounter​}​>​CLICK​ ); To create a custom hook we just copy out logic from the App component into a single function. One important rule about creating custom hooks is that their name needs to start with “use”. (like useFoo, useCounter, useEtc…), otherwise App fails to compile with an error: React Hook "useState" is called in function "counter" which is neither a React function component nor a custom React Hook function After extracting all the logic, out custom hook may look something like this: And finally it’s usage in the App component. 11 }; // Initial value can be passed to the hook as an argument function​ ​useCounter​(​initialValue​ = ​0​) { ​// Number counter ​const​ [​counter​, ​setCounter​] = ​useState​(​initialValue​); ​// Effect to set number into Title ​useEffect​(() ​=>​ { ​document​.​title​ = ​`​${​counter​}​`​; }, [​counter​]); ​// Handler for button onClick method ​const​ ​incrementCounter​ = () ​=>​ ​setCounter​(​p​ ​=>​ ++​p​); ​// Return only variables that we need to use outside of hook ​return​ [​counter​, ​incrementCounter​] ​as​ ​const​; } function​ ​App​() { ​const​ [​counter​, ​incrementCounter​] = ​useCounter​(​5​); ​return​ ( ​<​div​ ​className​=​'App'​> ​<​header​ ​className​=​'App-header'​> ​<​img​ ​src​=​{​logo​}​ ​className​=​'App-logo'​ ​alt​=​'logo'​ ​/> ​<​p​>​{​counter​}​ 12 ​<​button​ ​onClick​=​{​incrementCounter​}​>​CLICK​ ); }