Let's Deep Dive into Hooks

May 18, 2022

JavaScript
React

Since the adoption of Hooks developers found a new way of sharing logic across multiple siblings. With a less cumbersome approach than the typical Class pattern the behavior of passing down props are less convoluted. Hooks which are functions making the perfect mechanism for code reuse and sharing amongst the community.

For example:

1// Parent.jsx
2import React, { useState } from 'react';
3import Child from './Child';
4
5function Parent() {
6 const [parentCount, setParentCount] = useState(0);
7 console.log('Parent component re-rendering ------');
8 return (
9 <div style={{ background: 'lightseagreen' }}>
10 <Child />
11 <button
12 type="button"
13 onClick={() => {
14 setParentCount(parentCount + 1);
15 }}
16 >
17 Parent component +1
18 </button>
19 </div>
20 );
21}
22
23// Child.jsx
24function Child() {
25 console.log('------------ child component re-render');
26 return (
27 <div style={{ background: 'pink', margin: '50px 0' }}>
28 <button type="button"></button>
29 </div>
30 );
31}

When we click the button of the parent component, the state of the parent component parentCount will be updated, causing the parent component to re-render and the child component to re-render; at this time, there is no dependency between our child component and the parent component, so this repetition Rendering can be optimized out, you can use React.memo Wrap subcomponents


1// Child.jsx
2import React from 'react';
3// ...other code
4export default React.memo(Child);

React.memo(Comp[, fn])

  • Used to reduce re-rendering of child components
  • React.memo It is a higher-order component (the parameter is the component, and the function whose return value is the new component is the higher-order component)

For externally, the changes that React.memo will be checked, the component will be re-rendered props only when the incoming props changes, then we click the parent component button again, the child component will not be re-rendered

  • React.memo Only shallow comparisions are made for complex objects, and the comparison process can be controlled by passing in the second parameter
  • The second parameter is a function that receives props before and after re-rendering

1function MyComponent(props) {
2 /* render with props */
3}
4function areEqual(prevProps, nextProps) {
5 /*
6 If nextProps is passed to the render method, the return result is the same as
7 Returns true if the result of passing prevProps to the render method is consistent,
8 else return false
9 */
10}
11export default React.memo(MyComponent, areEqual);

useMemo(fn[, DependentArray])

  • Used to reduce repeated complex calculations every time the component is re-rendered, the parameters are a function and an optional array of dependecies, and return the result of the call to the passed function
  • const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
  • useMemo The function is similar to that Vue of computed (computed property), the difference is that the dependencies need to be passed in manually. When the dependencies change, the passed-in function will be called again and the calculated value will be returned.
  • When the incoming dependency is an empty array, the last calculation result is returned directly
  • When no dependencies are passed in, it will be recalculated every time the component is refreshed, which should be used as an optimization strategy when the code works properly

Modify our example, note that React.memo the sub-component is wrapped to ensure that the sub-component re-rendering is only affected by the incoming props changes during testing


1// Parent.jsx
2import React, { useState, useMemo } from 'react';
3import Child from '../Child';
4
5function Parent() {
6 const [parentCount, setParentCount] = useState(0);
7 const [otherCount, setOtherCount] = useState(0);
8 console.log('Parent component re-rendering --------------');
9
10 const computedFn = (a, b) => {
11 console.log('----Recalculated ----');
12 return a + b;
13 };
14
15 const computedValue = useMemo(() => {
16 return computedFn(parentCount, 1);
17 }, [parentCount]);
18
19 return (
20 <div style={{ background: 'lightseagreen' }}>
21 <Child parentCount={parentCount} computedValue={computedValue} />
22 <button
23 type="button"
24 onClick={() => {
25 setParentCount(parentCount + 1);
26 }}
27 >
28 Parent component +1
29 </button>
30 <button
31 type="button"
32 onClick={() => {
33 setOtherCount(otherCount + 1);
34 }}
35 >
36 Parent component otherCount+1
37 </button>
38 </div>
39 );
40}

Click the first button, the dependencies are changed, the output is recalculated, and the second button is clicked, because the change is not the dependency of the calculated value, so it will not be recalculated, and the child components will not be re-rendered


useCallback(fn[, DependentArray])

  • Used for functions that need to be passed to subcomponents to reduce repeated rendering of subcomponents, the parameters are a funciton and an optional array of dependencies, and return a memoized version of the function

1// Parent.jsx
2import React, { useState } from 'react';
3import Child from '../Child';
4
5function Parent() {
6 const [parentCount, setParentCount] = useState(0);
7 const [otherCount, setOtherCount] = useState(0);
8 console.log('Parent component re-rendering --------------');
9
10 const computedFn = () => {
11 return parentCount + 1;
12 };
13
14 return (
15 <div style={{ background: 'lightseagreen' }}>
16 <Child parentCount={parentCount} computedFn={computedFn} />
17 <button type="button" onClick={() => { setParentCount(parentCount + 1); }}>Parent component +1</button>
18 <button type="button" onClick={() => { setOtherCount(otherCount + 1); }}>Parent component otherCount+1</button>
19 </div>
20 );
21}
22
23export default Parent;
24
25// Child.jsx
26import React from 'react';
27
28function Child(props) {
29 const { computedValue, computedFn } = props;
30 console.log('------------ child component re-render');
31 return (
32 <div style={{ background: 'pink', margin: '50px 0' }}>
33 <div>
34 The calculation result passed in by the parent component:
35 {computedValue}
36 </div>
37 <button type="button" onClick={computedFn}>Subassembly</button>
38 </div>
39 );
40}
41
42export default React.memo(Child);

The child component also re-renders when the second button is clicked to computedFn add useCallback


1// Parent.jsx
2import React, { useState, useCallback } from 'react';
3
4// ...other code
5
6const computedFn = useCallback(() => {
7 console.log(parentCount);
8 return parentCount + 1;
9}, [parentCount]);
10
11// ...other code
12
13export default Parent;

At this time, click the second button sub-component of the parent component, the sub-component will not be re-rendered, because useCallback the dependencies of the sub-component have not changed, and the function of the last rendering is returned, so the props passed into the sub-component have not changed, and the component will not be re-rendered.

  • It should be noted that the useCallback internal scope of the saved function will not change. Therefore, when the dependency array is empty, useCallback the value of the variable in the component obtained through the closure inside the passed-in function will remain unchanged.

1import React, { useState, useCallback } from 'react';
2import Child from '../Child';
3
4let a = 0;
5function Parent() {
6 const [parentCount, setParentCount] = useState(0);
7 const [otherCount, setOtherCount] = useState(0);
8 console.log('Parent component re-rendering --------------');
9
10 const computedFn = useCallback(() => {
11 // The dependency is empty, the printed value here is always unchanged;
12 // Because the entire component will be re-rendered when the component
13 // state changes,
14 // and here parentCount always takes the value of the first rendered version
15 console.log(parentCount);
16 // The printed value here will be updated in real time, because the variable
17 // is defined directly outside the component and is not affected
18 // by the re-rendering of the component
19 console.log(a);
20 return parentCount + 1;
21 }, []);
22
23 return (
24 <div style={{ background: 'lightseagreen' }}>
25 <Child parentCount={parentCount} computedFn={computedFn} />
26 <button
27 type="button"
28 onClick={() => {
29 setParentCount(parentCount + 1);
30 a += 1;
31 }}
32 >
33 Parent component +1
34 </button>
35 <button
36 type="button"
37 onClick={() => {
38 setOtherCount(otherCount + 1);
39 }}
40 >
41 Parent component otherCount+1
42 </button>
43 </div>
44);
45}
46
47export default Parent;

  • Because the useCallback purpose is to reduce the re-rendering of sub-components, it needs to be used with sub-components shouldComponentUpdate or React.memo together to make optimization sense.
  • The above is the case when the dependecies are changed infrequently. When the dependencies are changed frequently, useCallback the memory effect is not good. It can be used ref as a dependency solution.

1function Form() {
2 const [text, updateText] = useState('');
3 const textRef = useRef();
4
5 useEffect(() => {
6 textRef.current = text; // write it to ref
7 });
8
9 const handleSubmit = useCallback(() => {
10 // ref objects remain the same for the lifetime of the component
11 // Read it from ref, the current change will not cause the component
12 // to re-render, and the correct value can be obtained inside the function
13 const currentText = textRef.current;
14 alert(currentText);
15 }, [textRef]);
16
17 return (
18 <>
19 <input value={text} onChange={(e) => updateText(e.target.value)} />
20 <ExpensiveTree onSubmit={handleSubmit} />
21 </>
22 );
23}

useRef

The conventional approach way of creating a ref:

1const refContainer = useRef(initialValue);

useRef Returns a mutable ref object whose .current properties are initialized to the passed parameter (initialValue). The returned ref object remains unchanged for the lifetime of the component


It can be understood as: useRef the object created by using has a current property, this property is like a box, and everything can be stored, including DOM nodes; the returned ref object remains unchanged during the entire life cycle of the component, that is, the existing current value is not subject to resetting by the component. The rendering effect always maintains the original reference; at the same time, the change of this property will not trigger the re-rendering of the component; the initial value of this property is useRef the parameter

Take a look at the official example:


1function TextInputWithFocusButton() {
2 const inputEl = useRef(null);
3 const onButtonClick = () => {
4 // `current` points to the text input element mounted on the DOM
5 inputEl.current.focus();
6 };
7 return (
8 <>
9 <input ref={inputEl} type="text" />
10 <button onClick={onButtonClick}>Focus the input</button>
11 </>
12 );
13}

When the useRef object is passed to DOM the ref attribute of the element, react will store DOM reference of the current element in the attribute, so that the element current can be directly manipulated through the ref object DOM


Conclusion

Today we learned about advantage Hooks gives us as a developer. Knowing we can asbtract pieces of logic and control the state in a more conceptual ergonomic way, we have a many ways to approach problems nowadays.

Resources:


my shoe
I’m Marlon but you can call me Mars. Software Engineer. Music lover. Bay Area Native. Feel free to Contact me.
  • implement promisify