The origin of the Hooks
Hooks were introduced to fix some long-standing problems with React:
- Logic with component state is difficult to reuse
To solve this problem, you need to introduce design patterns such as render props or higher-Order Components, such as the Connect method provided by React-Redux. This is not intuitive and requires a change in the hierarchy of components, with multiple wrapper calls nested in extreme cases.
Hooks allow easy reuse of stateful logic without changing the component hierarchy.
- Complex components are difficult to understand
A lot of business logic needs to be put in life cycle functions such as componentDidMount and componentDidUpdate, and often a life cycle function will contain multiple unrelated business logic, such as logging and data requests will be put in componentDidMount at the same time. On the other hand, the associated business logic may be placed in different lifecycle functions, such as subscribing to events when a component is mounted and unsubscribing when a component is unmounted, so you need to write the associated logic in both componentDidMount and componentWillUnmount.
Hooks can encapsulate the associated business logic, making code structure clearer.
- Difficult to understand Class components
The this keyword in JS is a bit of a pain in the neck. Unlike other object-oriented languages, its value is determined at runtime. To address this pain point, the arrow function’s this binding feature is available. React also has the concepts of Class Component and Function Component, which Component should be used when it is confusing. In terms of code optimization, precompiling and compressing Class Component is much more difficult and problematic than normal functions.
Hooks can use various features of React without introducing Class.
What is the Hooks
Hooks are functions that let you “hook into” React state and lifecycle features from function components
Here’s the official explanation. Hooks are functions. There are several types of Hooks, each providing a channel for Function Components to use React state and lifecycle features. Hooks cannot be used in Class Component.
React provides some predefined Hooks for us to use, so let’s look at them in more detail.
Commonly used hooks
State Hook
Let’s start with a traditional Class Component:
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
Rewriting with State Hook would look like this:
import React, { useState } from 'react';
function Example() {
// Define a State variable whose value can be changed by setCount
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()= > setCount(count => count + 1)}>
Click me
</button>
</div>
);
}
Copy the code
You can see that useState has only one entry, which is the initial value of state. This initial value can be a number, string, or object, or even a function. When the input parameter is a function, the function is executed only when the component is initially rendered:
const [state, setState] = useState(() = > {
const initialState = someExpensiveComputation(props);
return initialState;
});
Copy the code
The return value of useState is an array, the first element of which is the current value of state and the second element is the method to change state. There is no convention for naming these two variables. Note that if state is an object, setState does not automatically merge objects as Class Component setState does. To do this, do this:
setState(prevState= > {
// object. assign also works
return{... prevState, ... updatedValues}; });Copy the code
As you can see from the code above, setState can be a function as well as a number, string, or object. When we need to calculate the current state from the previous state, we need to pass in a function, similar to Class Component setState.
Another thing similar to Class Component setState is that when a new value is passed in the same way as the previous value (using object.is), no update is triggered.
- Note 1: If you want to optimize performance, such as when the initial internal logic is complex, change the useState initial value to the form returned by the function. This will only be resolved once at the initial time, not again at each rendering
- Note 2: setState should also be written as a function if possible. There are some minor drawbacks to writing setState as an object
- Note 3: setState cannot be locally updated if state is an object, because setState does not help us merge attributes
- If the setState address does not change, React will assume that the data has not changed
- Note 5:
// If you do, he will delete the age
const [user,setUser] = useState({name:'Frank'.age: 18})
const onClick = () = >{
setUser({
name: 'Jack'})}// If you want to make two calls in a row, add 2 once, use function notation, not object notation
const [n, setN] = useState(0)
const onClick = () = >{
// setN(n+1)
// setN(n+1) // You will find that n cannot be added by 2
setN(i= >i+1)
setN(i= >i+1)}Copy the code
Effect Hook
Before explaining this Hook, understand what a side effect is. Network requests, subscribing to a module, or DOM operations are examples of side effects that Effect Hook is designed to deal with. In normal cases, it is not recommended to write side effects in the body of a Function Component; otherwise, it is prone to bugs.
In the following Class Component example, the side effect code is written in componentDidMount and componentDidUpdate:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked The ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked The ${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
You can see that the code for componentDidMount is the same as for componentDidUpdate. This is not the case with Effect Hook rewriting:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Write this to indicate that the component is mounted and called each time it is updated
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
useEffect
Is executed after each DOM rendering without blocking the page rendering. It has bothcomponentDidMount
,componentDidUpdate
andcomponentWillUnmount
The execution timing of the three lifecycle functions.In addition, there are some side effects that require additional cleanup when a component is uninstalled, such as subscribing to a feature:
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
After subscribing at componentDidMount, you need to unsubscribe at componentWillUnmount. Rewriting with Effect Hook would look something like this:
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 a function for additional cleanup:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading... ';
}
return isOnline ? 'Online' : 'Offline';
}
Copy the code
When useEffect returns a function, React performs a cleanup before the next execution of the side effect.
Component mount -> Perform side effects -> Component update -> Perform cleanup function -> Perform side effects -> Component Update -> Perform cleanup function -> Component unload
As mentioned above, useEffect is performed after each render, but there are cases where we want to perform it only if the state or props changes. If it was a Class Component, we would do this:
componentDidUpdate(prevProps, prevState) {
if(prevState.count ! = =this.state.count) {
document.title = `You clicked The ${this.state.count} times`; }}Copy the code
When using Hook, we only need to pass in the second argument:
useEffect(() = > {
document.title = `You clicked ${count} times`;
}, [count]); // Effect is executed only when count changes
Copy the code
The second argument is an array that can pass multiple values, typically passing all the props and states used by Effect.
When side effects are only required at componentDidMount and componentWillUnmount, the second argument can be passed as an empty array [], which is somewhat similar to componentDidMount and componentWillUnmount.
useLayoutEffect
UseLayoutEffect is used in exactly the same way as useEffect, both of which can perform side effects and cleanup operations. The only difference is the timing of execution.
UseEffect does not block the browser’s drawing task, it does not execute until the browser has rendered and the page has been updated.
UseLayoutEffect blocks the rendering of a page the same way componentDidMount and componentDidUpdate execute. If you perform time-consuming tasks inside, the page will stagnate.
UseEffectHook is a better choice in most cases. The only exception is a scenario where DOM manipulation is required based on a new UI. UseLayoutEffect ensures that it is executed before the page is rendered, meaning that the page is rendered as it should be. If you use useEffect, the page is likely to shake because it was rendered twice.
- Note 1: Multiple
useEffect
In order, butuseLayoutEffect
There is always more thanuseEffect
To perform first - Note 2:
useEffect
Will be executed after the page is rendered, the following code will jitter from 0 to 1000, but if useduseLayoutEffect
Is executed before the page is rendered, rendering 1000 directly to the page
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const BlinkyRender = () = > {
const [value, setValue] = useState(0);
useEffect(() = > {
// execute after the page is mounted
document.querySelector('#x').innerText = `value: 1000`
}, [value]);
return (
<div id="x" onClick={()= > setValue(0)}>value: {value}</div>
);
};
ReactDOM.render(
<BlinkyRender />.document.querySelector("#root"));Copy the code
useContext
UseContext makes it easy to subscribe to context changes and re-render components when appropriate. Let’s get familiar with the standard context API usage:
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>); }}// The middle tier component
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// Subscribe by defining the static contextType attribute
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />; }}Copy the code
In addition to defining static properties, there is another way to subscribe to Function Components:
function ThemedButton() {
// Subscribe by defining Consumer
return (
<ThemeContext.Consumer>
{value => <Button theme={value} />}
</ThemeContext.Consumer>
);
}
Copy the code
Using useContext to subscribe, the code would look like this, with no extra layers and strange patterns:
function ThemedButton() {
const value = useContext(ThemeContext);
return <Button theme={value} />;
}
Copy the code
The benefits of useContext are especially evident when you need to subscribe to multiple contexts. Traditional implementation:
function HeaderBar() {
return (
<CurrentUser.Consumer>
{user =>
<Notifications.Consumer>
{notifications =>
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>}}</CurrentUser.Consumer>
);
}
Copy the code
The useContext implementation is more concise and intuitive:
function HeaderBar() {
const user = useContext(CurrentUser);
const notifications = useContext(Notifications);
return (
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>
);
}
Copy the code
Example:
const themes = {
light: {
foreground: "# 000000".background: "#eeeeee"
},
dark: {
foreground: "#ffffff".background: "# 222222"}};const ThemeContext = React.createContext(themes.light);
//themes.light initial default value
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
// Accept themes.dark passed in
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background.color: theme.foreground}} >
I am styled by theme context!
</button> );
}
Copy the code
useReducer
The use of useReducer is very similar to that of Redux. It is better to use useReducer than useState when the calculation logic of state is complex or when it needs to be calculated based on previous values. Here’s an example:
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() {
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
useCallback / useMemo / React.memo
React.memo
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () = > {
setN(n + 1);
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>// Every time the state n is updated, the Child component is updated, even if the state m on which it depends has not changed {/*<Child data={m}/>Child2 does not update every time state n is updated, because the state m on which it depends does not change<Child2 data={m}/>
</div>
);
}
function Child(props) {
console.log("Child executed");
console.log(Let's say I have a lot of code here.)
return <div>child: {props.data}</div>;
}
// Wrap the Child component in react. memo to keep the component-dependent state unchanged and the component will not update
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Copy the code
// Note that there is a bug in this app that breaks a second after adding a listener function to the child component, because the parent component changes causing the app function to be re-rendered, resulting in the creation of a new onClickChild that has the same function but a different address
import React from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () = > {
setN(n + 1);
};
const onClickChild = () = > {
console.log(m);
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child2 data={m} onClick={onClickChild} />{/* Child2 implements */}</div>
);
}
function Child(props) {
console.log("Child executed");
console.log("Let's say there's a lot of code here.");
return <div onClick={props.onClick}>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement)
Copy the code
The React solution is useCallback Hook. With the dependency unchanged, it returns the same reference, avoiding meaningless repeated rendering of child components:
const memoizedCallback = useCallback(
() = > { doSomething(a, b); }, [a, b],
);
Copy the code
`useCallback(fn, deps)`The equivalent of`useMemo(() => fn, deps)`.Copy the code
UseCallback caches method references, whereas useMemo caches method return values. Using scenes is to reduce unnecessary sub-component rendering:
const memoizedValue = useMemo(() = > () = > { doSomething(a, b); x}, [a, b]);
Copy the code
import React, { useMemo } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () = > {
setN(n + 1);
};
const onClick2 = () = > {
setM(m + 1);
};
//useMemo implements function reuse, receiving a function whose return value is what you want to cache
const onClickChild = useMemo(() = > {
return () = > {
console.log("on click child, m: " + m);
};
}, [m]); // If [m] is changed to [n], the old m is printed
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
<button onClick={onClick2}>update m {m}</button>
</div>
<Child2 data={m} onClick={onClickChild} />
</div>
);
}
function Child(props) {
console.log("Child executed");
console.log("Let's say there's a lot of code here.");
return <div onClick={e= > props.onClick(e.target)}>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Copy the code
-
UseCallBack is optimized for functions that can be recreated, so that functions are cached. The React. Memo assumes that the two addresses are the same to avoid redundant updates of child components.
-
UseMemo is optimized for unnecessary computation, eliminating some of the redundant computation operations in the current component.
useRef
UseRef is similar to React. CreateRef.
Using class components
class App extends React.Component {
refInput = React.createRef();
componentDidMount() {
this.refInput.current && this.refInput.current.focus();
}
render() {
return <input ref={this.refInput} />; }}Copy the code
Using function components
function App() {
const refInput = React.useRef(null);
React.useEffect(() = >{ refInput.current && refInput.current.focus(); } []);return <input ref={refInput} />;
}
Copy the code
Custom Hooks
Remember the problems we had with React in our last post? One of them is:
Logic with component state is difficult to reuse
This is solved by custom Hooks.
Continue with the example of subscribing to a friend’s status from the above article:
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
Let’s say I now have another component that has similar logic and is displayed in green when a friend is online. Simple copy and paste will do the trick, but it’s too inelegant:
import React, { useState, useEffect } from 'react';
function FriendListItem(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);
};
});
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
Copy the code
At this point we can define a custom Hook to encapsulate the logic of the subscription:
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
Custom hooks must be named with the prefix “use”, in which other hooks can be called. Both the input and return values can be customized as needed, with no special conventions. Like normal function calls, other hooks (such as useEffect) in a Hook are automatically called when appropriate:
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
A custom Hook is a normal function definition. It is named use to facilitate static code detection. I have to admire the React team for their clever design.
Hooks use rules
Two rules must be followed when using Hooks:
- Hooks can only be called in the first layer of code, not in loops, conditional branches, or nested functions.
- Only in the
Function Component
Or call Hooks from custom Hooks, which cannot be called from normal JS functions.
Hooks are designed to rely heavily on the order in which they are defined, and if the order in which Hooks are called changes in subsequent render, there are unpredictable problems. The above two rules are designed to keep the order of Hooks calls stable. To implement these two rules, React provides an ESLint plugin for static code detection: eslint-plugin-react-hooks.