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”.