I. Introduction to Hook
React Hooks are a new feature in React 16.8 designed to address state sharing and component lifecycle management confusion in React. React Hooks signal that React will no longer have stateless components. React will only have class and function components.
React Hook only shares the data processing logic, but does not share the data itself. Therefore, there is no need to worry about the binding of data to the life cycle. An example of implementing a counter using a class component is shown below.
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1})}> Click me </button> </div> ); }}Copy the code
As you can see, class components need to declare their own state, write methods to manipulate it, and maintain the life cycle of the state, which can be particularly cumbersome. React Hook provides the State Hook to handle the State, and the code will be much cleaner. The refactored code is shown below.
import React, { useState } from 'react';
function Example(a) {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Copy the code
As you can see, Example changes from a class component to a function component that has its own state and can update its state without calling the setState() method. This is possible because the class component uses the useState function.
Second, basic Hook
2.1 useState
The useState function is a built-in Hook function of React. The Hook function has the React state and lifecycle management capabilities. As you can see, the useState function takes only one input, the initial value of state, which can be a number, a string, an object, or even a function, as shown below.
function Example (props) {
const [ count, setCount ] = useState(() = > {return props.count || 0
})
return (
<div>
You clicked : { count }
<button onClick={() => { setCount(count + 1)}}>
Click me
</button>
</div>
)
}
Copy the code
Also, when the input parameter is a function, the function is executed only once during the initial rendering of the class component. If you need to operate on a state object at the same time, you can do so directly using a function that accepts the value of the state object and then performs an update operation, as shown below.
function Example(a) {
const [count, setCount] = useState(0);
function handleClick(a) {
setCount(count + 1)}function handleClickFn(a) {
setCount((prevCount) => {
return prevCount - 1})}return (
<>
You clicked: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>-</button>
</>
);
}
Copy the code
In the above code, handleClick and handleClickFn are both updated status values. React merges multiple state updates and updates the value of the state object all at once to save performance. In React application development, when a component’s state changes, it rerenders the entire component tree with that component as the root, as shown below.
function Child({ onButtonClick, data }) {
return (
<button onClick={onButtonClick}>{data.number}</button>
)
}
function Example () {
const [number, setNumber] = useState(0)
const [name, setName] = useState('hello')
const addClick = () => setNumber(number + 1)
const data = { number }
return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child onButtonClick={addClick} data={data} />
</div>
)
}
Copy the code
In the code above, the child refers to the data of the Number object, and when the data of the name object of the parent component changes, it also performs a redraw operation, even though nothing has changed for the child. In project development, to avoid repeated rendering of such unnecessary subcomponents, you need to wrap them using useMemo and useCallback, as shown below.
import {memo, useCallback, useMemo, useState} from "react";
function Child({ onButtonClick, data }) {
return (
<button onClick={onButtonClick}>{data.number}</button>
)
}
Child = memo(Child)
function Example () {
const [number, setNumber] = useState(0)
const [name, setName] = useState('hello')
const addClick = useCallback(() = >setNumber(number + 1), [number])
const data = useMemo(() => ({ number }), [number])
return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child onButtonClick={addClick} data={data} />
</div>
)
}
Copy the code
UseMemo and useCallback are two apis provided by React Hook, which are mainly used to cache data and optimize application performance. What they have in common is that they only call the callback function to recalculate the result if the dependent data changes. The differences are as follows.
- UseMemo: The cached result is the value returned by the callback function.
- UseCallback: Caches functions. Because functional components trigger entire component updates whenever state changes, some functional components that do not need to be updated are cached when useCallback is used.
In the example above, we pass function objects and an array of dependencies as arguments to useMemo, and because useMemo is used, the cached values are recalculated only when a dependency changes. After being optimized by useMemo and useCallback, the performance overhead of each rendering can be effectively avoided.
2.2 useEffect
Normally, network requests, module subscriptions, and DOM operations are side effects in the function body of the React function component. It is not recommended to write these side effects in the function body. Effect Hook is designed to deal with these side effects. The following is an example of implementing a counter using a class component, with the side effect code written in the componentDidMount and componentDidUpdate lifecycle functions, as shown below.
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1})}> Click me </button> </div> ); }}Copy the code
As you can see, the code in the life cycle functions componentDidMount and componentDidUpdate is the same. The same code comes up because, in many cases, we want to do the same thing when a component loads and updates. Conceptually, we would like to be able to merge it, but the class component disease does not provide such an approach. However, this problem can now be avoided using Effect Hook, as shown below.
import React, { useState, useEffect } from 'react';
function Example(a) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Copy the code
In fact, useEffect is executed only after each DOM rendering, so it does not block the rendering of the page. In addition, useEffect has the execution timing of life cycle functions such as componentDidMount, componentDidUpdate and componentWillUnmount. Also, we can use useEffect to access the state variable or props directly inside the component, so we can use useEffect to update the function value. In class components, subscription messages are typically set in the componentDidMount life cycle and cleared in the componentWillUnmount life cycle. For example, there is a ChatAPI module that subscribes to your friends’ online status, as shown below.
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading... ';
}
return this.state.isOnline ? 'Online' : 'Offline'; }}Copy the code
It can be found that componentDidMount and componentWillUnmount are corresponding, that is, the setting in the life cycle of componentDidMount needs to be removed in the life cycle of componentWillUnmount. However, handling module subscriptions manually can be quite cumbersome, and much easier if you use Effect Hook, as shown below.
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading... ';
}
return isOnline ? 'Online' : 'Offline';
}
Copy the code
In fact, every Effect returns a clean function. When useEffect returns a function, React performs a clean operation when the component is uninstalled. UseEffect is used after each render, but sometimes we want to render only if the state or props changes. Here’s how to write a class component.
if(prevState.count ! = =this.state.count) {
document.title = `You clicked ${this.state.count} times`; }}Copy the code
If React Hook is used, you just need to pass in the second argument, as shown below.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
Copy the code
As you can see, the second argument is an array in which you can pass all the props and states used by Effect. The second argument can be passed as an empty array if it needs to be executed only when the component is mounted and unmounted.
In addition to useEffect, useLayoutEffect can also perform side effects and cleanup operations. The difference is that useEffect is executed after the browser renders, whereas useLayoutEffect is executed before the browser renders.
2.3 useContext
In a class component, data sharing between components is implemented through the property props. In function components, since there is no concept of constructors and properties props, passing data between components can only be done using useContext. UseContext is a cross-level component data transfer method provided by React Hook that makes it easy to subscribe to context changes and re-render components when appropriate. UseContext can be used as follows.
const value = useContext(MyContext);
Copy the code
As you can see, useContext receives a context object context and returns the current value of that context object. The value of the current context object is determined by the data provider closest to the current component in the upper-layer component. The main purpose of useContext is to transfer data between components. First, create a new file named example.js and add the following code.
import { useState,createContext} from "react";
const CountContext = createContext()
function Example() {const [ count , setCount ] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()=>{setCount(count+1)}}>click me</button>
<CountContext.Provider value={count}>
</CountContext.Provider>
</div>
)
}
Copy the code
In the code above, we wrapped the count variable in a Provider, which allows it to pass component values across hierarchies, with child components changing when the parent component’s count variable changes. Once you have the context variable, you can use useContext to receive the value of the context variable. Create a new Counter component in example.js that displays the value of the context object count variable as follows.
function Counter(a){
const count = useContext(CountContext)
return (<h2>{count}</h2>)
}
Copy the code
Then, we also need to introduce the Counter component in the < countContext.provider > tag, as shown below.
<CountContext.Provider value={count}>
<Counter/>
</CountContext.Provider>
Copy the code
You can see that when you use useContext to pass values between components, you need to wrap variables that need to be passed around using a Provider.
Three other Hook
3.1 useReducer
As we all know, JavaScript Redux State management framework consists of Action, Reducer and Store, and Reducer is the only way to update the State in the component. Reducer is essentially a function that receives two parameters, the state and the judgment parameters that control the business logic, as shown below.
function countReducer(state, action) {
switch(action.type) {
case 'add':
return state + 1;
case 'sub':
return state - 1;
default:
returnstate; }}Copy the code
UseReducer is a function provided by React Hooks to replace useState in complex scenarios. For example, a state that contains complex logic and contains multiple child values, or a later state that depends on a previous state, and so on. The syntax format of useReducer is as follows.
const [state, dispatch] = useReducer(reducer, initialArg, init);
Copy the code
It can be found that the use of useReducer is very similar to that of Redux state framework. It receives a Reducer of the form (state, action) => newState and returns the current state and dispatch methods. For example, here is the code to implement a counter using useReducer.
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter(a) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
Copy the code
Sometimes, you need to create the initial state lazidly, and you simply pass in the initialization function as the third parameter to the useReducer, as shown below.
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {... .// omit other code
}
function Example({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return(...// omit other code
);
}
Copy the code
Sometimes, the return value of Reducer Hook is the same as the current state value. In this case, the rendering of subcomponents and the execution of side effects need to be skipped. Note that because React doesn’t unnecessarily render deep nodes in the component tree, you don’t have to worry about rendering the component again after skipping the rendering. If you want to avoid costly calculations during rendering, you can use useMemo for optimization.
3.2 useMemo
In a class component, each state update triggers a redrawing of the component tree, which inevitably incurs unnecessary performance overhead. Also, in the function component, React Hook provides the useMemo function to avoid the high computation overhead of useState each time it renders.
UseMemo provides a performance boost because useMemo returns the same reference when the dependency is unchanged, avoiding meaningless repeated rendering of child components. For example, here is an example of a common use of useState.
function Example(a) {
const [count, setCount] = useState(1);
const [val, setValue] = useState("); function expensive() { let sum = 0; for (let i = 0; i < count * 100; i++) { sum += i; } return sum; } return (<> {count} : {expensive()} setValue(event.target.value)}/> ); }Copy the code
In the example above, either changing the value of count or val triggers the execution of the Expensive method. However, because the execution of the Expensive method depends only on the value of count, there is no need to reevaluate when changing the value of val. So, to avoid this unnecessary calculation, you can use useMemo to optimize the code above, as shown below.
function Example(a) {
const [count, setCount] = useState(1);
const [val, setValue] = useState("); const expensive = useMemo(() => { let sum = 0; for (let i = 0; i < count * 100; i++) { sum += i; } return sum; }, [count]); Return (<> {count} : {expensive()} setValue(event.target.value)}/> ); }Copy the code
In the above code, we use useMemo to handle the time-consuming calculation, then pass the result to count and trigger a state refresh. After useMemo processing, count only triggers a time-consuming calculation execution state refresh when it changes, while changing val does not.
3.3 useCallback
Like useMemo, useCallback is used for performance optimization, that is, only when the dependent data changes will the callback function recalculate the result. The difference is that useMemo is primarily used to cache the value of the computed result, whereas useCallback caches functions. The syntax format of useCallback is as follows. Const fnA = useCallback(fnB, [a]) const fnA = useCallback(fnB, [a]) in the above statement, useCallback returns the function fnB passed to it and caches the result of the fnB run. Also, new functions are returned when dependent a changes. Since the returned function is a function, it is impossible to determine whether the returned function has changed. Therefore, we need to use the new data type Set of ES6 to assist in judgment, as shown below.
function Example(a) {
const [count, setCount] = useState(1);
const [val, setVal] = useState("); const callback = useCallback(() => { }, [count]); set.add(callback); return (
{count}:{set.size}
setVal(e.target.value)}/>
); }Copy the code
As you can see, set.size increases by 1 each time count is changed, whereas useCallback relies on the count variable, so a new function is returned each time count changes. When val changes, set.size does not change, indicating that it returns the cached old function. Let’s look at another scenario: we have a parent component that contains a child component that accepts a function as props. In general, if the parent component is updated, then the child component will also perform the update, but in most scenarios, the update of the child component is not necessary. At this point we can use useCallback to return the cached function and pass the cached function as props to the child component, as shown below.
function Parent(a) {
const [count, setCount] = useState(1);
const [val, setVal] = useState("); const callback = useCallback(() => { return count; }, [count]); return (
{count}
setVal(e.target.value)}/>
); } function Child({callback}) { const [count, setCount] = useState(() => callback()); useEffect(() => { setCount(callback()); }, [callback]); return
{count}
}Copy the code
In fact, useEffect, useMemo, and useCallback all come with their own closures. That is, each time a component renders, they capture state information in the context of the component function, so all three hooks reflect the current state when used. If you want to get the last state of the component, you can use ref to get it.
3.4 useRef
In React development, the Ref is mainly used to obtain component instances or DOM elements. There are two main ways to create a Ref, namely createRef and useRef. Where a Ref created using createRef returns a new reference every time it renders, useRef returns the same reference every time it renders. Creating a Ref using createRef is primarily a class component. For example, here is an example of creating a Ref using the createRef() method, as shown below.
class Example extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(a); }componentDidMount() {
this.myRef.current.focus(a); }render() {
return <input ref={this.myRef} type="text" />;
}
}
Copy the code
If you use the React Hooks useRef() method to create a Ref, the code looks like this.
function Example(a) {
const myRef = useRef(null);
useEffect(() => {
myRef.current.focus(a); }, [])return <input ref={myRef} type="text"/ >; }Copy the code
A Ref created using the useRef method returns a referenced DOM object that will persist for the lifetime of the component.
Iv. Custom Hook
By customizing hooks, we can extract component logic into reusable functions. In the previous chat application, the code to display your friends’ online status using React Hook is shown below.
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading... ';
}
return isOnline ? 'Online' : 'Offline';
}
Copy the code
Now, suppose you have a contact list in your chat application that needs to be set to green when the user is online. To implement this requirement, create a new FriendListItem component and add the following code.
function FriendListItem(props) {... .// omit other identical code
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
Copy the code
You can see that the state logic between FriendStatus and FriendListItem is basically the same. In class components, there are two ways to share state logic between components: props and higher-order components. In React Hook, if you want to share the logic between two functions, you can customize a Hook to encapsulate the logic of the subscription, as shown below.
import React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
Copy the code
In React, a custom Hook is a function that starts with use. Other hooks can be called inside the function. When a custom Hook is defined, the input parameter and return value can be customized as required. The state logic that needs to be shared has now been extracted into the custom Hook of useFriendStatus. We can then call the custom Hook function just like a normal function, as shown below.
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading... ';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
Copy the code
Custom hooks are a mechanism for reusing state logic, so every time a custom Hook is used, all states and side effects are completely isolated. Again, custom hooks need to be named with the beginning of use, again for static code detection tools.
5. Hook Rules
A Hook is essentially a JavaScript function, but there are two rules to follow when using it.
- Hooks are only used at the top level; hooks cannot be called in loops, conditions, or nested functions.
- You can only call a Hook in the React function, not in normal JavaScript functions.
The design of Hooks relies heavily on the order in which the events are defined, and if the order in which Hooks are called changes during a later rendering session, unpredictable problems can occur. Eslint-plugin-react-hooks EsLint plugin esLint plugin eslint-plugin-react-hooks esLint plugin esLint-plugin-react-hooks To use it, you need to add this plug-in to the React project, as shown below.
npm install eslint-plugin-react-hooks --save-dev
Copy the code
After the installation is complete, you will see the following configuration script in the package.json configuration file.
{
"plugins": [...// Omit the other plug-in packages
"react-hooks"]."rules": {...// Omit other rules
"react-hooks/rules-of-hooks": "error".// Check the Hook rules
"react-hooks/exhaustive-deps": "warn" // Check for effect dependencies}}Copy the code
After the above configuration, if the code does not conform to the Hook specification, the system will give the corresponding warning and prompt the developer to make the corresponding change.