This article explains in detail the new features released by React after version 16.8, and makes code demonstration of some commonly used Hooks, hoping to provide some help to friends in need.
preface
This article has been posted on Github: github.com/beichensky/… Welcome to Star!
I. Hooks introduction
Hooks are new in React V16.7.0-alpha. It lets you use state and other React features outside of class. This article is to demonstrate the use of the various Hooks API. I won’t go into details about the internals here.
Ii. Hooks first experience
Foo.js
import React, { useState } from 'react';
function Foo() {
// Declare a new state variable named "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Foo;
Copy the code
UseState is a Hook that allows us to have its own state without using the class component, and we can modify the state to control the presentation of the UI.
Three, the two commonly used Hooks
1, useState
grammar
const [state, setState] = useState(initialState)
- Pass a unique argument:
initialState
, can be a number, string, etc., can also be an object or an array. - Returns an array of two elements: the first element,
state
Variables,setState
Modify thestate
Value method.
And used in classessetState
Similarities and differences:
- Similarities: Called multiple times during a rendering cycle
setState
, the data changes only once. - Differences: in class
setState
Is merged while the function componentsetState
Is replaced.
Use the compare
Previously, to use the internal state of a component, you had to use the class component, for example:
Foo.js
import React, { Component } from 'react';
export default class Foo extends 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
Now we can do the same with functional components. This means that state can also be used inside functional components.
Foo.js
import React, { useState } from 'react';
function Foo() {
// Declare a new state variable named "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Foo;
Copy the code
To optimize the
Creating the initial state is expensive, so we can avoid recreating the ignored initial state by passing in a function when using the useState API.
The common way:
// Pass a value directly, and each time render executes the createRows function to get the return value
const [rows, setRows] = useState(createRows(props.count));
Copy the code
Optimized method (recommended) :
// createRows will only be executed once
const [rows, setRows] = useState(() = > createRows(props.count));
Copy the code
2, useEffect
Previously, many operations with side effects, such as network requests, UI modifications, and so on, were performed in the lifecycle of a class component such as componentDidMount or componentDidUpdate. There is no concept of these lifecycles in function components; only the element you want to render is returned. But now, there’s a place to do side effects in the function component, using the useEffect function.
grammar
useEffect(() => { doSomething });
Two parameters:
-
The first is a function that is a side effect of the first render and subsequent updates to the render.
- This function may have a return value, and if so, the return value must be a function that is executed when the component is destroyed.
-
The second argument, which is optional, is an array containing some of the side effects used in the first function. Use to optimize useEffect
- If you use this optimization, make sure that the array contains outer scopes that change over time and
effect
Any value used. Otherwise, your code will refer to the old value from the previous rendering. - If you want to run
effect
And clean it up only once (on load and unload), you can pass an empty array ([]) as the second argument. This tellsReact
youreffect
It doesn’t depend on the sourceprops
或state
So it never needs to be rerun.
- If you use this optimization, make sure that the array contains outer scopes that change over time and
While passing [] is closer to the familiar componentDidMount and componentWillUnmount execution rules, we recommend not using it as a habit, as it often results in errors.
Use the compare
Let’s say we have a requirement that the title of the document be consistent with the count count in the Foo component.
Using class components:
Foo.js
import React, { Component } from 'react';
export default class Foo extends 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
Side effects can now be performed in function components as well.
Foo.js
import React, { useState, useEffect } from 'react';
function Foo() {
// Declare a new state variable named "count"
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() = > {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Foo;
Copy the code
Not only that, you can use useEffect to perform multiple side effects (you can use one useEffect to perform multiple side effects, or you can perform them separately)
useEffect(() = > {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
const handleClick = () = > {
console.log('Mouse click');
}
useEffect(() = > {
// Bind the click event to the window
window.addEventListener('click', handleClick);
});
Copy the code
Now it looks like it’s done. But with class components, we typically remove registered events and so on in the componentWillMount lifecycle. How do you do this in a function component?
useEffect(() = > {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
const handleClick = () = > {
console.log('Mouse click');
}
useEffect(() = > {
// Bind the click event to the window
window.addEventListener('click', handleClick);
return () = > {
// Remove the click event from the window
window.removeEventListener('click', handleClick); }});Copy the code
As you can see, the first argument we pass in can return a function that is automatically executed when the component is destroyed.
Optimize useEffect
We’ve been using the first argument in useEffect, passing in a function. What about the second argument to useEffect?
The second argument to useEffect is an array containing the state value used in useEffect, which can be used as an optimization. The useEffect is executed only if the state value in the array changes.
useEffect(() = > {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
}, [ count ]);
Copy the code
If you want to simulate the behavior of componetDidMount and not componentDidUpdate, then pass a [] to the second argument to useEffect. (This is not recommended, however, as errors may occur due to omissions)
4. Other Hooks APIS
1, useContext
grammar
const value = useContext(MyContext);
Accepts a context object (the value returned from React.createcontext) and returns the current context value for that context. The current context value is determined by the prop nearest value above the calling component in the tree < myContext.provider >.
UseContext (MyContext) is equivalent to static contextType = MyContext in class, or < myContext.consumer >.
usage
Create a context in the app.js file and pass the context to the child component Foo
App.js
import React, { createContext } from 'react';
import Foo from './Foo';
import './App.css';
export const ThemeContext = createContext(null);
export default() = > {return (
<ThemeContext.Provider value="light">
<Foo />
</ThemeContext.Provider>)}Copy the code
In the Foo component, use the useContext API to get the incoming context value
Foo.js
import React, { useContext } from 'react';
import { ThemeContext } from './App';
export default() = > {const context = useContext(ThemeContext);
return (
<div>Foo component: The current theme is {context}</div>)}Copy the code
Matters needing attention
UseContext must be an argument to the context object itself:
- Correct:
useContext(MyContext)
- Incorrect:
useContext(MyContext.Consumer)
- Incorrect:
useContext(MyContext.Provider)
UseContext (MyContext) only allows you to read the context and subscribe to its changes. You still need < myContext.provider > to use the above content in the tree to provide the value for this context.
2, useReducer
grammar
const [state, dispatch] = useReducer(reducer, initialArg, init);
An alternative to useState. Accepts the Reducer whose type is (state, action) => newState and returns the current state of the reducer paired with dispatch.
UseReducer is usually superior to useState when you are dealing with complex state logic with multiple subvalues.
usage
Foo.js
import React, { useReducer } from 'react';
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();
}
}
export default() = > {// Use the useReducer function to create the state and dispatch function to update the state
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<br />
<button onClick={()= > dispatch({type: 'increment'})}>+</button>
<button onClick={()= > dispatch({type: 'decrement'})}>-</button>
</>
);
}
Copy the code
Optimization: Delayed initialization
You can also lazily create the initial state. To do this, you can pass the init function as the third argument. The initial state is set to init(initialArg).
It allows you to extract the logic used to calculate the initial state outside the Reducer. This is also handy for resetting the state later in response to the operation:
Foo.js
import React, { useReducer } from 'react';
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
export default ({initialCount = 0= > {})const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<br />
<button
onClick={()= > dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={()= > dispatch({type: 'increment'})}>+</button>
<button onClick={()= > dispatch({type: 'decrement'})}>-</button>
</>
);
}
Copy the code
The difference with useState
- when
state
When the state value structure is more complex, useuseReducer
More advantage. - use
useState
To obtain thesetState
Method updates data asynchronously; While the use ofuseReducer
To obtain thedispatch
Method update data is synchronized.
To illustrate the second difference, in the useState example above, we add a button:
UseState Foo in js
import React, { useState } from 'react';
function Foo() {
// Declare a new state variable named "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()= > setCount(count + 1)}>
Click me
</button>
<button onClick={()= >{ setCount(count + 1); setCount(count + 1); }}> < span style = "box-sizing: border-box; color: RGB (74, 74, 74)</button>
</div>
);
}
export default Foo;
Copy the code
If you click the button to see if you can add the data twice, you will see that the count is only increased by 1 when you click the button once.
In the example of useReducer usage above, we add a new button: foo.js in useReducer
import React, { useReducer } from 'react';
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();
}
}
export default() = > {// Use the useReducer function to create the state and dispatch function to update the state
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<br />
<button onClick={()= > dispatch({type: 'increment'})}>+</button>
<button onClick={()= > dispatch({type: 'decrement'})}>-</button>
<button onClick={()= >{ dispatch({type: 'increment'}); dispatch({type: 'increment'}); }}> < span style = "box-sizing: border-box; color: RGB (74, 74, 74)</button>
</>
);
}
Copy the code
Click the button to test whether it can be added twice, and it will be found that, click once, the count increases by 2. Therefore, each dispatch of an action will update data once, useReducer is indeed synchronous update data;
3, useCallback
grammar
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
The return value memoizedCallback is a Memoized callback. Pass inline callbacks and a set of dependencies. UseCallback returns a memoized version of the memory that changes only if one of the dependencies changes.
This is useful when passing a callback to an optimized sub-component that relies on reference equality to prevent unnecessary rendering (such as shouldComponentUpdate).
When using PureComponent and Memo with child components, you can reduce unnecessary rendering times for the child components
usage
-
Pass functions to child components without using useCallback
Foo.js
import React from 'react'; const Foo = ({ onClick }) = > { console.log('Foo:'.'render'); return <button onClick={onClick}>Foo component button</button> } export default Foo Copy the code
Bar.js
import React from 'react'; const Bar = ({ onClick }) = > { console.log('the Bar:.'render'); return <button onClick={onClick}>Bar component button</button>; }; export default Bar; Copy the code
App.js
import React, { useState } from 'react'; import Foo from './Foo'; import Bar from './Bar'; function App() { const [count, setCount] = useState(0); const fooClick = () = > { console.log('Button of component Foo was clicked'); }; const barClick = () = > { console.log('Button of Bar component was clicked'); }; return ( <div style={{ padding: 50}} > <p>{count}</p> <Foo onClick={fooClick} /> <br /> <br /> <Bar onClick={barClick} /> <br /> <br /> <button onClick={()= > setCount(count + 1)}>count increment</button> </div> ); } export default App; Copy the code
When we click on any of the above Count Increment buttons, we will see that the console prints two outputs, and the Foo and Bar components will be rerendered. But in our current logic, Foo and Bar components don’t need to render at all
Now we use useCallback for optimization
-
Use the optimized version of useCallback
Foo.js
import React from 'react'; const Foo = ({ onClick }) = > { console.log('Foo:'.'render'); return <button onClick={onClick}>Foo component button</button>; }; export default React.memo(Foo); Copy the code
Bar.js
import React from 'react'; const Bar = ({ onClick }) = > { console.log('the Bar:.'render'); return <button onClick={onClick}>Bar component button</button>; }; export default React.memo(Bar); Copy the code
App.js
import React, { useCallback, useState } from 'react'; import Foo from './Foo'; import Bar from './Bar'; function App() { const [count, setCount] = useState(0); const fooClick = useCallback(() = > { console.log('Button of component Foo was clicked'); } []);const barClick = useCallback(() = > { console.log('Button of Bar component was clicked'); } []);return ( <div style={{ padding: 50}} > <p>{count}</p> <Foo onClick={fooClick} /> <br /> <br /> <Bar onClick={barClick} /> <br /> <br /> <button onClick={()= > setCount(count + 1)}>count increment</button> </div> ); } export default App; Copy the code
If you click on the Count Increment button, you can see that the console has no output.
If you remove useCallback or React. Memo, you can see that the corresponding component will render unnecessarily again
4, useMemo
grammar
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Returns a memoized value. Pass the create function and the array of dependencies. UseMemo only recalculates the memoized value when one of the dependencies changes. This optimization helps to avoid expensive calculations on each rendering.
Functions passed by useMemo during rendering are run. Don’t do things you wouldn’t normally do when rendering. For example, side effects belong to useEffect, not useMemo.
usage
- You can cache the data, something like
Vue
的computed
, which can be automatically recalculated based on dependency changes - This can help us optimize the rendering of sub-components, such as the case where there are two sub-components Foo and Bar in the App component, which are passed from the App component to the component Foo
props
When a change occurs, the state of the App component changes and is rerendered. The Foo component and Bar component will also be rerendered. In fact, this situation is a waste of resources, we can now useuseMemo
Is used by the Foo componentprops
Only the Foo component changesrender
Bar does not rerender.
Example:
Foo.js
import React from 'react';
export default ({ text }) => {
console.log('Foo:'.'render');
return <div>Foo component: {text}</div>
}
Copy the code
Bar.js
import React from 'react';
export default ({ text }) => {
console.log('the Bar:.'render');
return <div>Bar component: {text}</div>
}
Copy the code
App.js
import React, { useState } from 'react';
import Foo from './Foo';
import Bar from './Bar';
export default() = > {const [a, setA] = useState('foo');
const [b, setB] = useState('bar');
return (
<div>
<Foo text={ a} / >
<Bar text={ b} / >
<br />
<button onClick={() = >SetA (' modified Foo')}> Modify the properties passed to Foo</button>
<button onClick={() = >SetB (' modified Bar')}> Modifies the property passed to Bar</button>
</div>)}Copy the code
When we click on any of the above buttons, we will see that the console prints two outputs, and components A and B will be rerendered.
Now we use useMemo for optimization
App.js
import React, { useState, useMemo } from 'react';
import Foo from './Foo';
import Bar from './Bar';
import './App.css';
export default() = > {const [a, setA] = useState('foo');
const [b, setB] = useState('bar');
+ const foo = useMemo(() = > <Foo text={ a} / >, [a]);
+ const bar = useMemo(() = > <Bar text={ b} / >, [b]);
return (
<div>+ {/ *<Foo text={ a} / >
+ <Bar text={ b} / > */}
+ { foo }
+ { bar }
<br />
<button onClick={() = >SetA (' modified Foo')}> Modify the properties passed to Foo</button>
<button onClick={() = >SetB (' modified Bar')}> Modifies the property passed to Bar</button>
</div>)}Copy the code
When we click on different buttons, the console will only print one output, change a or B, and only one of the a and B components will be rerendered.
UseMemo (() => fn, deps)
5, useRef
grammar
const refContainer = useRef(initialValue);
UseRef returns a mutable ref object whose.current property is initialized to the passed parameter (initialValue). The returned object persists throughout the lifetime of the component.
- In essence,
useRef
Like a “box” that can be in its.current
Property maintains a variable value. useRef Hooks
This applies not only to DOM references. The “ref” object is a generic container, whichcurrent
Attributes are mutable and can hold any value (they can be elements, objects, primitive types, or even functions), similar to instance attributes on a class.useRef
Closure penetration
Note: useRef() is more useful than the ref attribute. Similar to the way you use the instance field in a class, it is easy to retain any mutable values.
Note that useRef does not notify you when the content changes. Mutating the.current property does not cause a re-render. If you want to run some code when React appends or detags references to DOM nodes, you might need to use callback references.
usage
The following example shows how elements and strings can be stored in the current of the ref generated by useRef()
Example.js
import React, { useRef, useState, useEffect } from 'react';
export default() = > {// Create inputEl with useRef
const inputEl = useRef(null);
const [text, updateText] = useState(' ');
// Create textRef with useRef
const textRef = useRef();
useEffect(() = > {
// Store the text value in textref.current
textRef.current = text;
console.log('textRef. Current:, textRef.current);
});
const onButtonClick = () = > {
// `current` points to the mounted text input element
inputEl.current.value = "Hello, useRef";
};
return (
<>{/* Save input ref to inputEl */}<input ref={ inputEl } type="text" />
<button onClick={ onButtonClick} >Display text on input</button>
<br />
<br />
<input value={text} onChange={e= > updateText(e.target.value)} />
</>
);
}
Copy the code
Click the Show Text on Input button, and you’ll see Hello useRef on the first input; Enter something in the second input, and you can see the console print it out.
6, useLayoutEffect
grammar
useLayoutEffect(() => { doSomething });
Similar to useEffect Hooks, they perform side effects. But it is triggered after all DOM updates have been completed. Can be used to perform some of the layout related side effects, such as getting the DOM element width and height, window scroll distance, and so on.
UseEffect is preferred for side effects so as not to block visual updates. For side effects that are not DOM related, use useEffect.
usage
Use is similar to useEffect. But it will be executed before useEffect
Foo.js
import React, { useRef, useState, useLayoutEffect } from 'react';
export default() = > {const divRef = useRef(null);
const [height, setHeight] = useState(100);
useLayoutEffect(() = > {
// Print the height of the div when the DOM update is complete
console.log('useLayoutEffect: ', divRef.current.clientHeight);
})
return <>
<div ref={ divRef } style={{ background: 'red', height: height}} >Hello</div>
<button onClick={() = >SetHeight (height + 50)}</button>
</>
}
Copy the code
7, useImperativeHandle
In a function component, there is no instance of the component, so there is no way to call a state or method in a child component by binding an instance of the child component, as in a class component.
So in function components, how do you call the state or method of the child component from the parent component? The answer is useImperativeHandle
grammar
useImperativeHandle(ref, createHandle, [deps])
-
The first argument is the ref value, which can be passed through the attribute or used with the forwardRef
-
The second argument is a function that returns an object whose properties are mounted to the current property of the first argument ref
-
The third argument is the set of dependent elements, the same as useEffect, useCallback, and useMemo. When the dependency changes, the second argument is reexecuted, remounted to the current attribute of the first argument
usage
Note:
- The third parameter, dependency, must be filled in as required. Any less will result in an exception to the returned object property. Any more will result in an exception to the returned object property
createHandle
repeat - A component or
hook
In, for the sameref
Can only be used onceuseImperativeHandle
, many times, the following executionuseImperativeHandle
的createHandle
The return value replaces the one previously executeduseImperativeHandle
的createHandle
The return value
Foo.js
import React, { useState, useImperativeHandle, useCallback } from 'react';
const Foo = ({ actionRef }) = > {
const [value, setValue] = useState(' ');
/** * Randomly modify the value of the function */
const randomValue = useCallback(() = > {
setValue(Math.round(Math.random() * 100) + ' '); } []);/** * submit function */
const submit = useCallback(() = > {
if (value) {
alert('Submitted successfully, user name:${value}`);
} else {
alert('Please enter user name! ');
}
}, [value]);
useImperativeHandle(
actionRef,
() = > {
return {
randomValue,
submit,
};
},
[randomValue, submit]
);
/ *!!!!! To return more than one property and to write it this way, UseImperativeHandle (actionRef, () => {return {submit,}}, [submit]) useImperativeHandle(actionRef, () => { return { randomValue } }, [randomValue]) */
return (
<div className="box">
<h2>Function component</h2>
<section>
<label>User name:</label>
<input
value={value}
placeholder="Please enter your user name."
onChange={e= > setValue(e.target.value)}
/>
</section>
<br />
</div>
);
};
export default Foo;
Copy the code
App.js
import React, { useRef } from 'react';
import Foo from './Foo'
const App = () = > {
const childRef = useRef();
return (
<div>
<Foo actionRef={childRef} />
<button onClick={()= >Childref.current.submit ()}> Calls the child component's submit function</button>
<br />
<br />
<button onClick={()= >ChildRef. Current. RandomValue ()} > random modify child components of the input value</button>
</div>
);
};
Copy the code
Try to write custom Hooks
Here we make a custom Hooks that copy the official useReducer.
1. Write a custom useReducer
Create a new usereducer.js file in the SRC directory:
useReducer.js
import React, { useState } from 'react';
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
Copy the code
Tip: Hooks can be used not only within function components, but also within other Hooks.
2. Use a custom useReducer
Ok, the custom useReducer is finished, let’s see if it can be used normally?
Overwrite Foo component
Example.js
import React from 'react';
// From the custom useReducer
import useReducer from './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();
}
}
export default() = > {// Use the useReducer function to create the state and dispatch function to update the state
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<br />
<button onClick={()= > dispatch({type: 'increment'})}>+</button>
<button onClick={()= > dispatch({type: 'decrement'})}>-</button>
</>
);
}
Copy the code
V. Hooks use and write specification
-
Do not call Hooks from regular JavaScript functions;
-
Do not call Hooks inside loops, conditions, or nested functions.
-
Hooks must be called at the top of the component;
-
You can call Hooks from the React function component.
-
Hooks can be called from within custom Hooks.
-
It is a convention that custom Hooks must start with use.
Use the React ESLint plugin
As stated in the previous paragraph, there are certain rules to follow when using Hooks in React. However, in the process of writing code, these usage rules may be ignored, resulting in some uncontrollable errors. In this case, we can use the ESLint plugins provided by React: eslint-plugin-React-hooks. Here’s how to use it.
Install the ESLint plug-in
$ npm install eslint-plugin-react-hooks --save
Copy the code
Use plug-ins in.eslintrc
// Your ESLint configuration
// "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
// "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
{
"plugins": [
"react-hooks"]."rules": {
"react-hooks/rules-of-hooks": "error"."react-hooks/exhaustive-deps": "warn"}}Copy the code
Vii. Reference documents
The React website
React Hooks FAQ
Write in the back
If there is a wrong or not rigorous place to write, you are welcome to put forward valuable comments, thank you very much.
If you like or help, please welcome Star, which is also a kind of encouragement and support to the author.