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:closure
Yes returned by Hooksstate
andsetState
Method, 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 updatebaseState
: initialState is initializedbaseUpdate
: Action for the last call to update the state methodqueue
: Queue of scheduled operations waiting to enter the reducernext
: 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 mentionmemoizedState
Does this field look familiar? It is also found in the above definition of hook. Yes, it is also found in fiber data structure.memoizedState
Which points to the first hook in the hooks queue that belongs to this FibermemoizedState
Is 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