An overview of

React uses class or function definitions to create components:

There are many React features available in class definitions, such as state, various component lifecycle Hooks, etc., but there is nothing we can do in function definitions, so React 16.8 has a new function that includes the following: The React feature could be better used in function definition components.

Benefits:

The Hooks are the lightest API that allows you to use multiple components. They are the same as the render props/HOC API. Hooks are the lightest API that allows you to use multiple components.

2. More complex class definitions: different life cycles make logic decentralized and confusing, which is difficult to maintain and manage; Always pay attention to the direction of this; The cost of code reuse is high, and the use of higher-order components often makes the whole component tree become bloated.

State and UI isolation: Because of Hooks, the state logic is reduced to smaller granularity and easily abstracts into a custom Hooks, making state and UI in components clearer and more isolated.

Note:

Avoid using hooks in loop/conditional/nested functions to keep the order of calls stable. Only function-defined components and hooks can call hooks. Avoid calling hooks from class components or normal functions. Do not use useState in useEffect. React generates an error message. Class components will not be replaced or discarded, there is no need to forcibly modify class components, the two ways can coexist;

Important hooks:

  • UseState: Used to define the State of the component, benchmarking the functionality of this. State in the class component
  • UseEffect: A hook function triggered by a dependency. It is used to simulate the componentDidMount, componentDidUpdate, and componentWillUnmount methods in class components
  • Other built-in hooks :useContext: Gets the context object
  • UseReducer: an implementation similar to the idea of Redux, but not an adequate replacement for Redux. It can be understood as an internal Redux of a component. It is not persistent and will be destroyed when the component is destroyed. It belongs to the internal components, each component is isolated from each other, and it cannot share data; With the global nature of useContext, you can create a lightweight Redux
  • UseCallback: Caches the callback function so that the incoming callback is not a new function instance each time, causing the dependent component to re-render, which has the effect of performance optimization;
  • UseMemo: used to cache incoming props so that dependent components are not re-rendered each time;
  • UseRef: get the real node of the component;
  • UseLayoutEffect: DOM update synchronization hook. The usage is similar to useEffect except for the point in time of execution. UseEffect is asynchronous and does not wait for DOM to render, whereas useLayoutEffect does not trigger until DOM has rendered. You can get the updated state;
  • Custom Hooks (useXxxxx): We can write custom Hooks based on Hooks that reference other Hooks.

Array is used to simulate the implementation principle of useState

React hooks: Not Magic, Just Arrays We can use Array to emulate the principles of useState, as described in React Hooks: Not Magic, Just Arrays

When useState is called, a meta-ancestor of the form (variable, function) is returned. And the initial value of state is the argument passed in when useState is called externally.

Now that we’ve sorted out the parameters and return values, let’s take a look at what useState does. As shown in the code below, when the button is clicked, setNum is executed, the state num is updated, and the UI view is updated. Obviously, the function useState returns to change the state automatically calls the Render method to trigger the view update.

function App() {
  const [num, setNum] = useState(0);

  return (
    <div>
      <div>num: {num}</div>
      <button onClick={()= >SetNum (num + 1)}> add 1</button>
    </div>
  );
}

Copy the code

A preliminary simulation

function render() {
  ReactDOM.render(<App />.document.getElementById("root"));
}

let state;

function useState(initialState){
  state = state || initialState;

  function setState(newState) {
    state = newState;
    render();
  }

  return [state, setState];
}

render(); // First render
Copy the code

Preliminary simulations lead us to the first core principle of Hooks:closureYes returned by HooksstateandsetStateMethod, which are implemented using closures inside hooks

However, real useXXX can be declared multiple times, so our initial implementation here does not support multiple variable declarations

Why not use hooks inside loops and judgments

First, Array is used to simulate the React Hook principle

In the previous simple implementation of useState, the initial state was stored in a global variable. By analogy, multiple states should be kept in a dedicated global container. This container is just an unpretentious Array object. The specific process is as follows:

  • On the first rendering, state by state is declared and placed into the global Array in useState order. Each time you declare state, increase the cursor by one.
  • Update state to trigger rendering again. The cursor is reset to 0. The view is updated by fetching the latest state values in the order in which useState is declared.

For example:

function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi");
  const [lastName, setLastName] = useState("Yardley");

  return (
    <Button onClick={()= > setFirstName("Fred")}>Fred</Button>
  );
}
Copy the code

The creation flow of the code above

1) Initialization

Create two arrays, setters and state, set cursor = 0;

2) First render

Go through all the useState and put setterspush in the array and statepush in the state array

3) Re-render

Each subsequent rerender resets the cursor = 0 and retrieves the previous state from the array in turn

4) Event triggering

Each event has a state value for the cursor, and any state event that is triggered modifies the corresponding state value in the state array

A complete simulation of useState

import React from "react";
import ReactDOM from "react-dom";

const states = [];
let cursor = 0;

function useState(initialState) {
  const currenCursor = cursor;
  states[currenCursor] = states[currenCursor] || initialState; // Check if it has been rendered

  function setState(newState) {
    states[currenCursor] = newState;
    render();
  }

  cursor+=1; // Update the cursor
  return [states[currenCursor], setState];
}

function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(1);

  return (
    <div>
      <div>count1: {count1}</div>
      <div>
        <button onClick={()= > setCount1(count1 + 1)}>add1 1</button>
        <button onClick={()= > setCount1(count1 - 1)}>delete1 1</button>
      </div>
      <hr />
      <div>num2: {num2}</div>
      <div>
        <button onClick={()= > setCount2(count2 + 1)}>add2 1</button>
        <button onClick={()= > setCount2(count2 - 1)}>delete2 1</button>
      </div>
    </div>
  );
}

function render() {
  ReactDOM.render(<App />.document.getElementById("root"));
  cursor = 0; / / reset the cursor
}

render(); // First render
Copy the code

Use Hooks in loop, judgment

let firstRender = true;

function RenderFunctionComponent() {
  let initName;
  
  if(firstRender){
    [initName] = useState("Rudi");
    firstRender = false;
  }
  const [firstName, setFirstName] = useState(initName);
  const [lastName, setLastName] = useState("Yardley");

  return (
    <Button onClick={()= > setFirstName("Fred")}>Fred</Button>
  );
}
Copy the code

Create a process diagram

Heavy rendering

As you can see, because of firstRender’s condition, the cursor 0 state is set to useState(initName), the cursor 1 state is set to useState(“Yardley”), Yardley is actually the value of state with cursor 2

That is, when the component is initialized, the hooks directly maintain an array of state and setState methods, which, if used in conditional rendering, will cause the cursor to be rerendered and the setState method to be inactivated

The implementation principle of useEffect is simulated

UseEffect is the second most frequently used hook method after useState, which is used by hooks that need to listen to and perform certain operations when state or props changes. ComponentDidMount, componentDidUpdate, componentWillUnmount methods

Simulation implementation (still using Array + Cursor idea)

const allDeps = [];
let effectCursor = 0;

function useEffect(callback, deps = []) {
  if(! allDeps[effectCursor]) {// First render: assign + call callback function
    allDeps[effectCursor] = deps;
    effectCursor+=1;
    callback();
    return;
  }

  const currenEffectCursor = effectCursor;
  const rawDeps = allDeps[currenEffectCursor];
  // Check if the dependency has changed, which requires rerender
  const isChanged = rawDeps.some(
    (dep,index) = >dep ! == deps[index] );// Dependency changes
  if (isChanged) {
    // Perform the callback
    callback();
    // Modify the new dependency
    allDeps[effectCursor] = deps;
  }
  // cursor increment
  effectCursor+=1;
}

function render() {
  ReactDOM.render(<App />.document.getElementById("root"));
  effectCursor = 0; // Notice that effectCursor is reset to 0
}
Copy the code

Real React implementation

We simulated the implementation of Hooks using arrays, but the actual implementation of React uses a single list instead of arrays and next to chain all Hooks together

First let’s look at a picture

Dispatcher

Dispatcher is a shared object that contains hooks functions. It will be allocated or cleaned dynamically based on the render phase of the ReactDOM, and it will ensure that users cannot access Hooks outside the React component, source reference

Hooks are enabled or disabled by a flag variable called enableHooks, which are checked when rendering the root component and simply switched to the appropriate Dispatcher, source reference

Part of the source

function renderRoot(root: FiberRoot, isYieldy: boolean) :void { invariant( ! isWorking,'renderRoot was called recursively. This error is likely caused ' +
      'by a bug in React. Please file an issue.',); flushPassiveEffects(); isWorking =true;
  // Control the current Dispatcher for hooks
  if (enableHooks) {
    ReactCurrentOwner.currentDispatcher = Dispatcher;
  } else{ ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks; }...Copy the code

When rendering is complete, the dispatcher will be set to null, this is to prevent abnormal access outside the ReactDOM rendering, source reference

Part of the source

 // We're done performing work. Time to clean up.
  isWorking = false;
  ReactCurrentOwner.currentDispatcher = null;
  resetContextDependences();
  resetHooks();
Copy the code

Within Hooks, the current Dispatcher reference is resolved using the resolveDispatcher method, and an error is reported if the current Dispatcher is abnormal

Part of the source

function resolveDispatcher() {
  constdispatcher = ReactCurrentOwner.currentDispatcher; invariant( dispatcher ! = =null.'Hooks can only be called inside the body of a function component.',);return dispatcher;
}
Copy the code

The real Hooks

The Dispatcher is an external unified exposure controller for the Hooks mechanism. During the rendering process, the Dispatcher is controlled by the flag of the current context. The core meaning of the Dispatcher is to strictly control the rendering of the Hooks, so as to prevent the Hooks from being called where there are exceptions

hooks queue

Hooks represent nodes that are linked together in the order in which they are called. To summarize some of the attributes of hooks

  • The initial render creates the initial state
  • Status values can be updated
  • React remembers the previous state values after rerendering
  • React gets and updates the correct state in the order it is called
  • React knows which fiber the current hook belongs to

So when we look at Hooks, we don’t think of each hook node as an object, but as a linked list node, and the entire Hooks model as a queue

{
  memoizedState: 'foo'.next: {
    memoizedState: 'bar'.next: {
      memoizedState: 'baz'.next: null}}}Copy the code

We can see the source code for a Hook and Effect model definition, source code

export type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any> | null.queue: UpdateQueue<any> | null.next: Hook | null}; type Effect = {tag: HookEffectTag,
  create: () = > mixed,
  destroy: (() = > mixed) | null.inputs: Array<mixed>, next: Effect, }; .export function useState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  return useReducer(
    basicStateReducer,
    // useReducer has a special case to support lazy useState initializers
    (initialState: any),
  );
}
Copy the code

First of all, it can be seen that the implementation of useState is the implementation of a certain situation of useReducer, so in the official document, also said that useReducer is another implementation of useState, combined with the idea of Redux, can avoid too much transfer of callback function, Instead, you can send dispatches directly to the underlying component

Here I will post a case about the use of useReducer. In fact, it is mainly to understand the principle and use of Redux or DVA, you can mark the use of useReducer

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  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

Returning to the Hook definition, we can now specify each parameter

  • memoizedState: cache state after hook update
  • baseState: initialState is initialized
  • baseUpdate: Action for the last call to update the state method
  • queue: Queue of scheduled operations waiting to enter the reducer
  • next: Link to the next hook and concatenate each hook with next

See hooks in combination with Fiber

React in V16, the mechanism for building and rendering components was changed from stack mode to Fiber mode, and changed to single-list tree traversal with linked lists and Pointers. Through pointer mapping, a record of every unit traverse on the current step and the next step, which made the traversal can be suspended or restart Understand here is the division of a task scheduling algorithm, the original synchronous update rendering task split into separate small task unit, according to the different priorities, scatter small task to the browser’s free time, Take advantage of the main process’s time loop

Fiber is simply the concept of a basic task cutting unit for component rendering that contains the most basic task content unit for the current component build

There is an important concept to mentionmemoizedStateDoes this field look familiar? It is also found in the above definition of hook. Yes, it is also found in fiber data structure.memoizedStateWhich points to the first hook in the hooks queue that belongs to this FibermemoizedStateIs the state value of the current hook cache.

We can look at the source code

// There's no existing queue, so this is the initial render.
  if (reducer === basicStateReducer) {
    // Special case for `useState`.
    if (typeof initialState === 'function') { initialState = initialState(); }}else if(initialAction ! = =undefined&& initialAction ! = =null) {
    initialState = reducer(initialState, initialAction);
  }
  // Note: key
  workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;
Copy the code

As you can see above, initialState, as the initialState value, is assigned to both baseState and memoizedState

Look at three paragraphs of source code, source link

// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let firstCurrentHook: Hook | null = null;
let currentHook: Hook | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
Copy the code
export function prepareToUseHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  nextRenderExpirationTime: ExpirationTime,
) :void {
  if(! enableHooks) {return; } renderExpirationTime = nextRenderExpirationTime; currentlyRenderingFiber = workInProgress; firstCurrentHook = current ! = =null ? current.memoizedState : null;

  // The following should have already been reset
  // currentHook = null;
  // workInProgressHook = null;

  // remainingExpirationTime = NoWork;
  // componentUpdateQueue = null;

  // isReRender = false;
  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;
}
Copy the code
export function finishHooks(Component: any, props: any, children: any, refOrContext: any,) :any {
  if(! enableHooks) {return children;
  }

  // This must be called after every function component to prevent hooks from
  // being used in classes.

  while (didScheduleRenderPhaseUpdate) {
    // Updates were scheduled during the render phase. They are stored in
    // the `renderPhaseUpdates` map. Call the component again, reusing the
    // work-in-progress hooks and applying the additional updates on top. Keep
    // restarting until no more updates are scheduled.
    didScheduleRenderPhaseUpdate = false;
    numberOfReRenders += 1;

    // Start over from the beginning of the list
    currentHook = null;
    workInProgressHook = null;
    componentUpdateQueue = null;

    children = Component(props, refOrContext);
  }
  renderPhaseUpdates = null;
  numberOfReRenders = 0;

  const renderedWork: Fiber = (currentlyRenderingFiber: any);

  renderedWork.memoizedState = firstWorkInProgressHook;
  renderedWork.expirationTime = remainingExpirationTime;
  renderedWork.updateQueue = (componentUpdateQueue: any);
  
  constdidRenderTooFewHooks = currentHook ! = =null&& currentHook.next ! = =null;

  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  firstCurrentHook = null;
  currentHook = null;
  firstWorkInProgressHook = null;
  workInProgressHook = null;

  remainingExpirationTime = NoWork;
  componentUpdateQueue = null; .Copy the code

One of the sources has this comment: The Hooks are stored as a linked list on the Fiber’s memoizedState field

The second piece of code is fiber, where the hook performs the pre-function

The third piece of code is in the fiber, rear hook to perform functions, method have so a renderedWork. MemoizedState = firstWorkInProgressHook;

So let’s sum it up

There is memoizedState in both the Hook data structure and the Fiber data structure, but the meanings are different. In Hook, there is memoizedState as the state value of the cache, but in Fiber, there is the first Hook of the hooks queue under the current fiber. Means access to the entire hooks queue.)

CurrentlyRenderingFiber = workInProgress; currentlyRenderingFiber = workInProgress;

firstCurrentHook = current ! == null ? current.memoizedState : null;

These two lines of code assign the currently rendered Fiber and first hook of the currently executing hooks queue to the current global variables currentlyRenderingFiber and firstCurrentHook, respectively

Take a look at the source code for the currentlyRenderingFiber variable

// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber | null = null;
Copy the code

CurrentlyRenderingFiber defines the fiber structure currently being rendered

If you want to save the memoizedState field of the current Fiber to firstWorkInProgressHook, you can see that the memoizedState field of the current Fiber is saved to the firstWorkInProgressHook. Then set the currentlyRenderingFiber field to null

Class or Hooks

In the current environment, Hooks have gradually become the mainstream component mode, such as ant4. x component, which has been fully recommended. Hooks are mainly of simplified coding mode and functional programming idea, while Calss component is mainly of [complete], [precise] component flow control. This includes strict control over rendering using shouldComponentUpdate and other lifecycle controls

The Class components

In business development, the thinking mode is: [what to do first, then what to do]. The parameter of the second callback of this.setState is the absolute embodiment of this idea, and then complete the function of a whole component with [life cycle function]. For component encapsulation and reuse, HOC mode must also rely on Class implementation

Hooks

Using Hooks for the standard Class component requires a programming paradigm shift. The Hooks business development paradigm is:

After all the states are maintained, I need to think about the [side effects] generated around these states. When my state or props changes, I need to do the corresponding [side effects]. Under this design concept, UseEffect can be directly referenced to Class components for a collection of componentDidMount, componentDidUpdate, componentWillUnmount methods

But classes are not replaceable at the moment, because classes have full life cycle control, shouldComponentUpdate and so on, whereas Hooks don’t have such fine-grained control

State logic can be easily isolated and reused using Hooks.

  • It is easier to reuse code: Hooks are common JavaScript functions, so developers can combine the built-in hooks into custom hooks that handle state logic, so complex problems can be converted into a single-responsibility function that can be used by the entire application or React community;
  • It is more elegant to use composition: unlike patterns like render props or high-order components, hooks do not introduce unnecessary nesting in the component tree and are not negatively affected by mixins;
  • Less code: A useEffect performs a single duty, eliminating duplicate code in a lifecycle function. By avoiding splitting the same responsibility code into several lifecycle functions, better reuse helps good developers minimize the amount of code;
  • Clear code logic: hooks help developers split components into functional units with separate functions, making code logic clearer and easier to understand.

【 Tips: Function has no instance and cannot be controlled by ref, but Hooks can use react. forwardRef to pass refs to function and useImperativeHandle to selectively expose child instances to parent components.

Reference:

React Hooks

The React principle of Hooks

Under the hood of React’s hooks system

The React – ReactFiberHooks source code