The React principle of the hooks
Basic preparation
usingcreact-react-appCreate a project
Have put the project on Github: github.com/Sunny-lucki… Can you humble yourself and ask for a star
Handwritten useState
The use of useState
UseState can add a state Hook to a function component.
Calling useState returns a state variable and a method to update the state variable. The parameter of useState is the initial value of the state variable, which is only valid for the first rendering.
The method that updates the state variable does not merge the states as this.setstate does. I’m replacing the state variable. Here is a simple example that will render the value of count on the page. Clicking the setCount button will update the value of count.
function App(){
const [count, setCount] = useState(0);
return (
<div>
{count}
<button
onClick={()= >{ setCount(count + 1); }} ></button>
</div>
);
}
ReactDOM.render(
<App />.document.getElementById('root'));Copy the code
Principle of implementation
let lastState
function useState(initState) {
lastState = lastState || initState;
function setState(newState) {
lastState = newState
}
return [lastState,setState]
}
function App(){
/ /...
}
ReactDOM.render(
<App />.document.getElementById('root'));Copy the code
As the code shows, we created a useState method ourselves
When we use this method, we take the value of initState if we use it for the first time, and the last value (lastState) otherwise.
Internally, we created a setState method that updates the value of state
It then returns a lastSate property and a setState method.
It looks perfect, but we’re missing the point: every time we play setState, we should rerender the current component.
So we need to do a refresh inside setState
let lastState
function useState(initState) {
lastState = lastState || initState;
function setState(newState) {
lastState = newState
render()
}
return [lastState,setState]
}
function App(){
const [count, setCount] = useState(0);
return (
<div>
{count}
<button
onClick={()= >{ setCount(count + 1); }} ></button>
</div>
);
}
// Add a new method
function render(){
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
As shown in the code, we added a render method to the setState. The render method executes
ReactDOM.render(
<App />.document.getElementById('root'));Copy the code
So it’s a re-render.
Okay, is it complete now?
No, here’s another question: let’s say we only use one useState here, what if we use many? Do you want to declare a lot of global variables?
This is obviously not possible, so we can design a global array to hold these states
let lastState = []
let stateIndex = 0
function useState(initState) {
lastState[stateIndex] = lastState[stateIndex] || initState;
const currentIndex = stateIndex
function setState(newState) {
lastState[currentIndex ] = newState
render()
}
return [lastState[stateIndex++],setState]
}
Copy the code
CurrentIndex here uses the idea of closures to record an index corresponding to a state.
Ok, so that’s basically the end of the useState method. So easy!
The React. Memo is introduced
Look at the code below! What problems did you find?
import React ,{useState}from 'react';
import ReactDOM from 'react-dom';
import './index.css';
function Child({data}) {
console.log("Oh, my God, how am I being portrayed? I don't want to be portrayed.")
return (
<div>child</div>)}function App(){
const [count, setCount] = useState(0);
return (
<div>
<Child data={123}></Child>
<button onClick={()= >{setCount(count + 1)}}> add</button>
</div>
);
}
function render(){
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
Sure, we want to render the child component when the parent’s props change, even though we passed the props to the child component with fixed values.
So React. Memo was introduced.
Look at introduction
React.memo() is similar to PureComponent in that it helps us control when to rerender the component.
A component is rerendered only if its props have changed. In general, the React component in the component tree goes through the render process whenever there is a change. But with PureComponent and React.memo(), we can render only certain components.
import React ,{useState,memo}from 'react';
import ReactDOM from 'react-dom';
import './index.css';
function Child({data}) {
console.log("Oh, my God, how am I being portrayed? I don't want to be portrayed.")
return (
<div>child</div>
)
}
Child = memo(Child)
function App(){
const [count, setCount] = useState(0);
return (
<div>
<Child data={123}></Child>
<button onClick={()= >{setCount(count + 1)}}> add</button>
</div>
);
}
function render(){
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
Therefore, when the Child is wrapped by the memo, it will only be rerendered if the props change.
Of course, since react. memo is not part of the React-hook, how it is implemented is not discussed here.
Handwritten useCallback
The use of useCallback
When we try to pass a method to a child component, the code below looks like this
import React ,{useState,memo}from 'react';
import ReactDOM from 'react-dom';
function Child({data}) {
console.log("Oh, my God, how am I being portrayed? I don't want to be portrayed.")
return (
<div>child</div>)}// eslint-disable-next-line
Child = memo(Child)
function App(){
const [count, setCount] = useState(0);
const addClick = () = >{console.log("addClick")}
return (
<div>
<Child data={123} onClick={addClick}></Child>
<button onClick={()= >{setCount(count + 1)}}> add</button>
</div>
);
}
function render(){
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
It turns out that we passed an addClick method that is fixed, but the subcomponent is rerendered every time the button is clicked.
This is because it looks like the addClick method hasn’t changed, but the old and new addClick methods are different, as shown in the figure below
In this case, if you want to pass in the same method, you use useCallBack.
This is shown in the code
import React ,{useState,memo,useCallback}from 'react';
import ReactDOM from 'react-dom';
function Child({data}) {
console.log("Oh, my God, how am I being portrayed? I don't want to be portrayed.")
return (
<div>child</div>)}// eslint-disable-next-line
Child = memo(Child)
function App(){
const [count, setCount] = useState(0);
// eslint-disable-next-line
const addClick = useCallback(() = >{console.log("addClick")}, [])return (
<div>
<Child data={123} onClick={addClick}></Child>
<button onClick={()= >{setCount(count + 1)}}> add</button>
</div>
);
}
function render(){
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
The first argument to the useCallback hook is the method we want to pass to the child component. The second argument is an array to listen for changes to the elements in the array before returning a new method.
Principle of implementation
We know that useCallback takes two arguments, so we can write it first
function useCallback(callback,lastCallbackDependencies){}Copy the code
As with useState, we need to store callback and Dependencies as global variables.
let lastCallback
let lastCallbackDependencies
function useCallback(callback,dependencies){}Copy the code
First, useCallback determines if we passed a dependency, and if not, returns the latest callback each time we execute useCallback
let lastCallback
let lastCallbackDependencies
function useCallback(callback,dependencies){
if(lastCallbackDependencies){
}else{ // No dependencies were passed in
}
return lastCallback
}
Copy the code
So when we don’t pass in a dependency, we can actually execute it as if it were the first time, so we reassign lastCallback and lastCallbackDependencies
let lastCallback
let lastCallbackDependencies
function useCallback(callback,dependencies){
if(lastCallbackDependencies){
}else{ // No dependencies were passed in
lastCallback = callback
lastCallbackDependencies = dependencies
}
return lastCallback
}
Copy the code
When a dependency is passed in, you need to see if the value of each item in the new dependency array is equal to that of each item in the incoming dependency array
let lastCallback
let lastCallbackDependencies
function useCallback(callback,dependencies){
if(lastCallbackDependencies){
letchanged = ! dependencies.every((item,index) = >{
return item === lastCallbackDependencies[index]
})
}else{ // No dependencies were passed in
lastCallback = callback
lastCallbackDependencies = dependencies
}
return lastCallback
}
function Child({data}) {
console.log("Oh, my God, how am I being portrayed? I don't want to be portrayed.")
return (
<div>child</div>)}Copy the code
When the value of a dependency changes, we need to re-assign lastCallback and lastCallbackDependencies
import React ,{useState,memo}from 'react';
import ReactDOM from 'react-dom';
let lastCallback
// eslint-disable-next-line
let lastCallbackDependencies
function useCallback(callback,dependencies){
if(lastCallbackDependencies){
letchanged = ! dependencies.every((item,index) = >{
return item === lastCallbackDependencies[index]
})
if(changed){
lastCallback = callback
lastCallbackDependencies = dependencies
}
}else{ // No dependencies were passed in
lastCallback = callback
lastCallbackDependencies = dependencies
}
return lastCallback
}
function Child({data}) {
console.log("Oh, my God, how am I being portrayed? I don't want to be portrayed.")
return (
<div>child</div>)}// eslint-disable-next-line
Child = memo(Child)
function App(){
const [count, setCount] = useState(0);
// eslint-disable-next-line
const addClick = useCallback(() = >{console.log("addClick")}, [])return (
<div>
<Child data={123} onClick={addClick}></Child>
<button onClick={()= >{setCount(count + 1)}}> add</button>
</div>
);
}
function render(){
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
Handwritten useMemo
use
UseMemo is similar to useCallback, except that useCallback is used to cache functions, and useMemo is used to cache return values from functions
let data = useMemo(() = > ({number}),[number])
Copy the code
As shown in the code, useMemo is used to cache the return value of the function number, and will only be reexecuted if the listener element is [number], that is, if the value of number changes
()=> ({number})
Copy the code
And then return the new number
The principle of
Therefore, the principle of useMemo is similar to that of useCallback.
import React ,{useState,memo,}from 'react';
import ReactDOM from 'react-dom';
let lastMemo
// eslint-disable-next-line
let lastMemoDependencies
function useMemo(callback,dependencies){
if(lastMemoDependencies){
letchanged = ! dependencies.every((item,index) = >{
return item === lastMemoDependencies[index]
})
if(changed){
lastMemo = callback()
lastMemoDependencies = dependencies
}
}else{ // No dependencies were passed in
lastMemo = callback()
lastMemoDependencies = dependencies
}
return lastMemo
}
function Child({data}) {
console.log("Oh, my God, how am I being portrayed? I don't want to be portrayed.")
return (
<div>child</div>)}// eslint-disable-next-line
Child = memo(Child)
function App(){
const [count, setCount] = useState(0);
// eslint-disable-next-line
const [number, setNumber] = useState(20)
let data = useMemo(() = > ({number}),[number])
return (
<div>
<Child data={data}></Child>
<button onClick={()= >{setCount(count + 1)}}> add</button>
</div>
);
}
function render(){
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
Handwritten useReducer
use
Let’s just briefly introduce useReducer.
const [state, dispatch] = useReducer(reducer, initState);
Copy the code
UseReducer takes two parameters:
The first parameter is the Reducer function, and the second parameter is the initialized state.
The returned values are the latest state and dispatch functions (which are used to trigger the Reducer function to calculate the corresponding state).
According to official statement: useReducer is recommended for complex state operation logic and nested state objects.
If this sounds abstract, let’s look at a simple example:
// Official useReducer Demo
// The first parameter: the initialization of the application
const initialState = {count: 0};
// The second parameter is the reducer handler function for state
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() {
// Return values: the latest state and dispatch functions
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>// useReducer will return the final state and trigger the Rerender Count according to the Action of the Dispatch: {state.count} // dispatch receives the action parameter "Action in reducer", which triggers the Reducer function to update the latest state<button onClick={()= > dispatch({type: 'increment'})}>+</button>
<button onClick={()= > dispatch({type: 'decrement'})}>-</button>
</>
);
}
Copy the code
If state is a basic data type, use useState. If state is an object, use Reducer. This is a simple idea. You don’t have to take it personally. The specific situation depends on the specific scenario.
The principle of
Look at the principle and you’ll see that it’s very simple, so simple that I don’t need to say anything, less than ten lines of code, don’t believe you just look at the code
import React from 'react';
import ReactDOM from 'react-dom';
let lastState
/ / useReducer principle
function useReducer(reducer,initialState){
lastState = lastState || initialState
function dispatch(action){
lastState = reducer(lastState,action)
render()
}
return [lastState,dispatch]
}
// Official useReducer Demo
// The first parameter: the initialization of the application
const initialState = {count: 0};
// The second parameter is the reducer handler function for state
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() {
// Return values: the latest state and dispatch functions
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>{/* // useReducer returns the final state and triggers rerender */} Count: {state.count} {/* // dispatches the reducer to the latest state */}<button onClick={()= > dispatch({type: 'increment'})}>+</button>
<button onClick={()= > dispatch({type: 'decrement'})}>-</button>
</>
);
}
function render(){
ReactDOM.render(
<Counter />.document.getElementById('root')); } render()Copy the code
Handwritten useContext
use
CreateContext creates a React context and subscribesto components that receive data or other information from the context.
Basic usage:
const MyContext = React.createContext()
Copy the code
To use the created Context, you need to wrap the component in the outermost layer of context. Provider, and you need to display the value in < mycontext. Provider value={{xx:xx}}>. Specifies the information that the context is exposed to.
Provider-> Provider-> Provider-> Provider-> Provider-> Provider-> C
If ContextA and ContextB provide the same method, then the C component will only select the method provided by ContextB.
The context created with react.createcontext can be accessed by the Provider in the child component via the useContext Hook
const {funcName} = useContext(MyContext);
Copy the code
As you can see from the above code, useContext needs to pass the Context instance MyContext, either a string or the instance itself.
One awkward aspect of this usage is that the parent and child components are not in the same directory. How do you share the Context instance MyContext?
In this case, I would use the Context Manager to manage the Context instance, export the instance using export, and import the instance in the child component.
Let’s look at the code, which is pretty simple to use
import React, { useState, useContext } from 'react';
import ReactDOM from 'react-dom';
let AppContext = React.createContext()
function Counter() {
let { state, setState } = useContext(AppContext)
return (
<>
Count: {state.count}
<button onClick={()= > setState({ number: state.number + 1 })}>+</button>
</>
);
}
function App() {
let [state, setState] = useState({ number: 0 })
return (
<AppContext.Provider value={{ state.setState}} >
<div>
<h1>{state.number}</h1>
<Counter></Counter>
</div>
</AppContext.Provider>)}function render() {
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
For those of you who have used Vue, this mechanism is similar to provide and inject in Vue
The principle of
The principle is very simple. Since the createContext Provider is not the content of the ReactHook, the value needs to implement useContext, as shown in the code, in just one line of code
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
let AppContext = React.createContext()
function useContext(context){
return context._currentValue
}
function Counter() {
let { state, setState } = useContext(AppContext)
return (
<>
<button onClick={()= > setState({ number: state.number + 1 })}>+</button>
</>
);
}
function App() {
let [state, setState] = useState({ number: 0 })
return (
<AppContext.Provider value={{ state.setState}} >
<div>
<h1>{state.number}</h1>
<Counter></Counter>
</div>
</AppContext.Provider>)}function render() {
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
Handwritten useEffect
use
It has the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount, but is synthesized into an API.
import React, { useState, useEffect} from 'react';
import ReactDOM from 'react-dom';
function App() {
let [number, setNumber] = useState(0)
useEffect(() = >{
console.log(number);
},[number])
return (
<div>
<h1>{number}</h1>
<button onClick={()= > setNumber(number+1)}>+</button>
</div>)}function render() {
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
As the code shows, two arguments are supported, the second of which is also used for listening. The first argument of the execution function is to listen for changes in the elements of the array
The principle of
UseMemo, useCallback, useEffect, useEffect, useMemo, useecallback, useEffect, useEffect (There is also a return value for componentWillUnmount, but the return value is different because you don’t know how to write.
let xxx = useEffect(() = >{
console.log(number);
},[number])
Copy the code
To receive the return value.
So, ignoring the return value, you can just look at the code, and it’s really similar, it’s almost identical
import React, { useState} from 'react';
import ReactDOM from 'react-dom';
let lastEffectDependencies
function useEffect(callback,dependencies){
if(lastEffectDependencies){
letchanged = ! dependencies.every((item,index) = >{
return item === lastEffectDependencies[index]
})
if(changed){
callback()
lastEffectDependencies = dependencies
}
}else{
callback()
lastEffectDependencies = dependencies
}
}
function App() {
let [number, setNumber] = useState(0)
useEffect(() = >{
console.log(number);
},[number])
return (
<div>
<h1>{number}</h1>
<button onClick={()= > setNumber(number+1)}>+</button>
</div>)}function render() {
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
You think that’s it, but it’s not, because the first argument was executed at the wrong time, and actually the function that was the first argument was executed after the browser had rendered it. Here we’re doing it synchronously.
Instead, you need to execute the callback asynchronously
import React, { useState} from 'react';
import ReactDOM from 'react-dom';
let lastEffectDependencies
function useEffect(callback,dependencies){
if(lastEffectDependencies){
letchanged = ! dependencies.every((item,index) = >{
return item === lastEffectDependencies[index]
})
if(changed){
setTimeout(callback())
lastEffectDependencies = dependencies
}
}else{
setTimeout(callback())
lastEffectDependencies = dependencies
}
}
function App() {
let [number, setNumber] = useState(0)
useEffect(() = >{
console.log(number);
},[number])
return (
<div>
<h1>{number}</h1>
<button onClick={()= > setNumber(number+1)}>+</button>
</div>)}function render() {
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
Handwritten useLayoutEffect
use
The official explanation is that these two hooks are basically the same, but the invocation time is different. Please use useEffect for all of them, and consider using useLayoutEffect unless you encounter a bug or unsolvable problem.
The principle of
The principle is the same as useEffect, but the timing is different
UseEffect is called after the browser has finished rendering, while useLayoutEffect is called after the DOM has been built and before the browser has rendered.
So we need to change the macro task setTimeout to a microtask
import React, { useState} from 'react';
import ReactDOM from 'react-dom';
let lastEffectDependencies
function useLayouyEffect(callback,dependencies){
if(lastEffectDependencies){
letchanged = ! dependencies.every((item,index) = >{
return item === lastEffectDependencies[index]
})
if(changed){
Promise.resolve().then(callback())
lastEffectDependencies = dependencies
}
}else{
Promise.resolve().then(callback())
lastEffectDependencies = dependencies
}
}
function App() {
let [number, setNumber] = useState(0)
useLayouyEffect(() = >{
console.log(number);
},[number])
return (
<div>
<h1>{number}</h1>
<button onClick={()= > setNumber(number+1)}>+</button>
</div>)}function render() {
ReactDOM.render(
<App />.document.getElementById('root')); } render()Copy the code
Have you already put the project on Github: github.com/Sunny-lucki…
The article was published on the public account “Front Sunshine”.