Preface
Given that readers have a certain understanding of React, and all the cases in this book are written using React Hooks, and React features such as Context are used in the React Router source code, this chapter only introduces some features of React, such as Context and Hooks. For other React-related features, readers can refer to related materials to learn.
Context
In React, parent components usually pass data to child components as props. If you need to transfer data across layers, then using props to transfer data layer by layer will complicate development. At the same time, in actual development, many components need the same things, such as international language configuration, application theme colors, etc., and they do not want to pass these configuration information level by level during the process of developing components.
In this case, you can use React's Context feature. Context is translated into context, as literally, it contains information across the current level.
Context has a wide range of applications in many components or development libraries. For example, react-redux uses Context as a provider to provide a global store, and React Router provides routing state through Context. Mastering Context will greatly help understanding React Router. Here is Figure 3-1 to illustrate how Context transfers data across components.
In Figure 3-1, the component tree on the left uses props layer by layer to transfer data. Even if component B and component C do not need to care about a certain data item, they are forced to pass the data item as props to the child. Components. Context is used to achieve cross-level data transfer between components, and data can be directly transferred from component A to component D.
In React v16.3 and above, you can use the React.createContext interface to create a Context container. Based on the producer-consumer model, after the container is created, the container provider (generally called Provider) can be used to provide certain cross-level data, and the container consumer (generally called the Consumer) can be used to consume the data provided by the container provider. Examples are as follows:
// 传入defaultValue
// 如果Consumer没有对应的Provider,则Consumer所获得的值为传入的1
const CountContext = React.createContext(1);
class App extends React.Component {
state = { count: 0 };
render() {
console.log('app render');
return (
<CountContext.Provider value={this.state.count}>
<Toolbar />
<button onClick={() => this.setState(state => ({ count: state.count + 1 }))}>
更新
</button>
</CountContext.Provider>
);
}
}
Change the value of count through setState to trigger render rendering, and Context.Provider will pass the latest value to all Context.Consumers.
class Toolbar extends React.Component {
render() {
console.log('Toolbar render');
return (
<div>
<Button />
</div>
);
}
}
class Button extends React.Component {
render() {
console.log('Button outer render');
return (
// 使用Consumer跨组件消费数据
<CountContext.Consumer>
{count => {
// 在Consumer中,受到Provider提供数据的影响
console.log('Button render');
return <div>{count}</div>;
}}
</CountContext.Consumer>
);
}
}
In the above example, the top-level component App uses CountContext.Provider to provide the value of this.state.count to the descendant components. The Toolbar, a subcomponent of App, does not consume data provided by the Provider, and the Button, a subcomponent of Toolbar, uses CountContext.Consumer to obtain the data count provided by the App. The Toolbar component in the middle layer does not have any perception of data transfer across layers. When clicking the "Update" button to trigger data transfer, the "Toolbar render" information in the Toolbar will not be printed. Each time you click the "Update" button, only "app render" and "Button render" will be printed. This is because when the value provided by the Provider changes, only the Consumer will render, so the "Toolbar render" in the Toolbar will not Is printed.
If the Provider is also used to provide data in the Toolbar, for example, the value provided is 500:
class Toolbar extends React.Component {
render() {
console.log('Toolbar render');
return (
<CountContext.Provider value={500}>
<Button />
</CountContext.Provider>
);
}
}
The Consumer in the Button will get the value of 500. The reason is that when there are multiple Providers, the Consumer will consume the value provided by the nearest Provider in the component tree. As an important feature of React, this is widely used in React Router source code.
Note that if the value of Context.Provider is not set, or undefined is passed in, the Consumer will not obtain the defaultValue data when the Context is created. The defaultValue data when creating the Context is mainly provided to Consumers that do not match the Provider. If the Provider in the App is removed, the value obtained by the Consumer is 1.
If you want to use this.context to get the value provided by the Provider, you can declare the static property contextType (React v16.6.0) of the class. The value of contextType is the created Context, such as:
const MyContext = React.createContext();
class MyClass extends React.Component {
static contextType = MyContext;
render() {
// 获取Context的值
let value = this.context;
}
}
Before React v16.3, the creation of a context through createContext was not supported. You can use community polyfill solutions, such as create-react-context.
Note that component optimization methods such as shouldComponentUpdate or React.memo cannot affect the transfer of Context values. If shouldComponentUpdate is introduced in Button, it will prevent Button from updating:
shouldComponentUpdate() {
// 返回false 阻止了Button组件的渲染,但是Provider提供的数据依然会提供到
//Consumer中
// 不受此影响
return false;
};
After changing the value provided by the Provider, it will still trigger the re-rendering of Consumer, and the result is the same as when shouldComponentUpdate is not introduced.
Hooks
React Hooks is a feature officially introduced in React v16.8, which aims to solve the problems of logic reuse and sharing related to state.
Before the birth of React Hooks, with the iteration of the business, the life cycle function of the component was filled with all kinds of unrelated logic. The usual solution is to use Render Props to dynamically render the required parts, or use high-level components to provide common logic to decouple the logical associations between components. However, no matter which method it is, it will cause problems such as an increase in the number of components, modification of the component tree structure, or excessive nesting levels of components. After the birth of Hooks, it encapsulated the logic that was originally scattered in various life cycle functions to deal with the same business, making it more portable and reusable. Using Hooks not only makes it easier to reuse state logic between components, but also makes complex components easier to read and understand; and because there is no large amount of polyfill code for class components, only functional components can be run, Hooks will use less code Achieve the same effect.
React provides a large number of Hooks function support, such as useState to provide component state support, useEffect to provide side effects support, and useContext to provide context support.
When using React Hooks, you need to comply with the following guidelines and feature requirements.
- Use Hooks only at the top level. Do not call Hooks in loops, conditions, or nested functions. Make sure to always call them at the top level of React function components.
- Do not call Hooks in ordinary JavaScript functions. Only call Hooks in React's functional components, and call other Hooks in custom Hooks.
useState
useState is similar to state and setState in React components, which can maintain and modify the current component state.
useState is a hook function that comes with React. Use useState to declare internal state variables. The parameter that useState receives is the state initial value or state initialization method, and it returns an array. The first item of the array is the current state value, and its state value may be different each time it is rendered; the second item is the set function that can change the corresponding state value, and the function will not change after useState is initialized.
The type of useState is:
function useState<S>(initialState: S | (() => S)): [S, Dispatch <SetStateAction <S>>];
The initialState only takes effect when the component is initialized, and subsequent renderings will ignore the initialState:
const [inputValue, setValue] = useState("react");const [react, setReact] = useState(inputValue);
Like inputValue in the above example, when the initial value is passed into another state and initialized, another state function will no longer depend on the value of inputValue.
The way to use Hooks is very simple. After introduction, use it in function components:
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>您点击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}> 单击触发更新 </button>
</div>
);
}
Similar to setState, calling setCount when the button is clicked updates the state value count. After calling setCount, the component will re-render and the value of count will be updated.
When the incoming initial state is a function, it is executed only once, similar to the constructor in the class component:
const [count, setCount] = useState(() => {
// 可执行初始化逻辑
return 0;
});
In addition, the update function returned by useState can also use functional update:
setCount(preCount => preCount + 1)
If the new state needs to be calculated based on the previous state, then the callback function can be passed to setState as a parameter. The callback function will receive the previous state and update the returned value as the new state.
Note that React stipulates that Hooks must be written in the outermost layer of the function, and cannot be written in conditional statements such as if...else to ensure that the execution order of Hooks is consistent.
useEffect
side effect
In computer science, if certain operations, functions, or expressions modify the values of some state variables outside of their local environment, they are said to have side effects. The side effect can be a network request to communicate with a third party, or the modification of external variables, or the call of any other function with side effects. Side effects are not good or bad. Their existence may affect the use of other environments. What developers need to do is to properly handle the side effects and isolate the side effects from the rest of the program. This will make the entire software system easy to expand, refactor, and debug. , Testing and maintenance. In most front-end frameworks, developers are also encouraged to manage side effects and component rendering in separate, loosely coupled modules.
For functions, functions that are executed without side effects are called pure functions. They receive parameters and return values. Pure functions are deterministic, meaning that they always return the same output given the input. But this does not mean that all impure functions have side effects. For example, generating random values within a function will make a pure function an impure function, but it does not have side effects.
React is about pure functions, and it requires purity of render. If the render is not pure, it will affect other components and affect the rendering. But in the browser, side effects are everywhere. If you want to handle side effects in React, you can use useEffect. useEffect, as the name implies, is to perform operations with side effects, and its declaration is as follows:
useEffect(effect: React.EffectCallback, inputs?: ReadonlyArray<any> | undefined)
The first parameter of the function is the side effect function, and the second parameter is the dependent array for executing the side effect, which will be introduced in the following content. Examples are as follows:
const App = () => {
const [value, setValue] = React.useState(0);
// 引入useEffect
React.useEffect(function useEffectCallBack() {
// 可执行副作用
// 在此进行数据请求、订阅事件或手动更改 DOM等操作
const nvDom = document.getElementById('content');
console.log('color effect', nvDom.style.color);
});
console.log('render');
return (
<div
id="content"
style={{ color: value === 1 ? 'red' : '' }}
onClick={() => setValue(c => c + 1)}
>
{' '}
value: {value}{' '}
</div>
);
};
After the above components are initialized, the color effect will be printed once after the render is printed, indicating that the incoming effect is executed after the component is rendered. After clicking the element whose ID is content, the value state will be updated, a rendering will be triggered, and the color effect red will be printed after the render is printed. This process indicates that React's DOM has been updated, and control is passed to the developer's side-effect function. The side-effect function successfully obtains the updated value of the DOM. In fact, the above process is similar to React's componentDidMount and componentDidUpdate life cycles. React will call the function passed to useEffect for the first rendering and every subsequent rendering. This is where useEffect can be compared with traditional components. Generally speaking, useEffect can be compared to a collection of componentDidMount, componentDidUpdate, and componentWillUnmount, but note that they are not completely equivalent. The main difference is that the code in componentDidMount or componentDidUpdate is executed "synchronously". "Synchronization" here means that the execution of side effects will hinder the rendering of the browser itself. For example, sometimes it is necessary to calculate the size of an element based on the DOM before re-rendering. At this time, the life cycle method will occur before the browser actually draws. .
The execution of the side-effect functions defined in useEffect will not prevent the browser from updating the view, which means that these functions are executed asynchronously. The so-called asynchronous execution means that the callback function passed in to useEffect is triggered after the "drawing" phase of the browser, and it is not "synchronized" that hinders the browser's drawing. Under normal circumstances, this is more reasonable, because most of the side effects are not necessary to hinder the browser's drawing. For useEffect, React uses a special method to ensure that the effect function is triggered after the "drawing" phase:
const channel = new MessageChannel();
channel.port1.onmessage = function () {
// 此时绘制结束,触发effect函数
console.log('after repaint');
};
requestAnimationFrame(function () {
console.log('before repaint');
channel.port2.postMessage(undefined);
});
RequestAnimationFrame is used in combination with postMessage to achieve this kind of purpose.
In short, useEffect will be triggered after the browser finishes executing the reflow/repaint process. The effect function is suitable for side effects that do not depend on DOM and do not hinder the rendering of the main thread, such as data network requests and external event binding.
Clear side effects
When a side effect has some influence on the outside world, before executing the side effect again, the previous side effect should be cleared, and then the side effect should be updated again. In this case, a function can be returned in the effect, that is, the cleanup function.
Each effect can return a cleanup function. As an optional clearing mechanism for useEffect, it can put the logic of monitoring and canceling monitoring in one effect.
So, when does React clear the effect? The effect's cleanup function will be executed before the side effect function after the component is re-rendered. Take an example to illustrate:
const App = () => {
const [value, setValue] = useState(0);
useEffect(function useEffectCallBack() {
expensive();
console.log('effect fire and value is', value);
return function useEffectCleanup() {
console.log('effect cleanup and value is ', value);
};
});
return <div onClick={() => setValue(c => c + 1)}>value: {value}</div>;
};
Every time you click the div element, it will print:
// 第一次单击
effect cleanup and value is 0
effect fire and value is 1
// 第二次单击
effect cleanup and value is 1
effect fire and value is 2
// 第三次单击
effect cleanup and value is 2
effect fire and value is 3
// ……
As shown in the above example, React will clear the previous effect before executing the current effect. The variable values in the scope of the clear function are the variable values in the last rendering, which is related to the Caputure Value feature of Hooks, which will be introduced in the following content.
In addition to executing the cleanup function for each update, React also executes the cleanup function when the component is uninstalled.
Reduce unnecessary effects
As mentioned above, after each component rendering, the cleanup function and the corresponding side-effect function in the effect will be run. If these functions are executed every re-rendering, it is obviously not economical, and in some cases it may even cause an endless loop of side effects. At this time, you can use the second parameter in the useEffect parameter list to solve it. The second parameter in the useEffect parameter list is also called the dependency list. Its function is to tell React to execute the incoming side-effect function only when the parameter value in this list changes:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
// 只有当count的值发生变化时,才会重新执行document.title这一行
So, how does React determine that the value in the dependency list has changed? In fact, for each value in the dependency list, React will use Object.is to compare before and after the element to determine if there are any changes. If in the current rendering process, an element in the dependent list is different from the element in the previous rendering cycle, the effect side effect will be executed.
Note that if one of the elements is an object or an array, then Object.is will compare references to the object or array, which may cause some confusion:
function App({ config }) {
React.useEffect(() => {}, [config]);
return <div>{/* UI */}</div>;
}
// 每次渲染都传入config新对象
<App config={{ a: 1 }} />;
If config is passed in from the outside every time, although the field value of the config object remains unchanged, since the reference of the newly passed object is not equal to the reference of the previous config object, the effect side effect will be executed. To solve this problem, you can rely on some community solutions, such as use-deep-compare-effect.
Under normal circumstances, if the second parameter of useEffect is passed in an empty array [] (this is not a special case, it still follows the way the dependency list works), React will consider its dependent element to be empty, and every time it is rendered By comparison, there is no change between the empty array and the empty array. React believes that effect does not depend on any value in props or state, so effect side effects never need to be executed repeatedly, which can be understood as componentDidUpdate will never be executed. This is equivalent to executing the effect only during the first rendering, and executing the cleanup function when the component is destroyed. It should be noted that this is only an easy-to-understand analogy. For the difference between passing in an empty array [] as the second parameter and this kind of life cycle, please see the following notes.
Precautions
1) Capture Value feature
Note that React Hooks has the feature of Capture Value, and each rendering has its own props and state:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log('count is', count);
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
In useEffect, the initial value 0 is always obtained, and "count is 0" will always be printed; the value in h1 will always be the value of setCount(0+1), which is "1". If you want count to increase sequentially, you can use useRef to save count, useRef will be introduced in section 3.2.4.
2) async function
useEffect is not allowed to pass in async functions, such as:
useEffect(async () => {
// return函数将不会被调用
}, []);
The reason is that the async function returns a promise, which is easily confused with the cleanup function of useEffect. Returning the cleanup function in the async function will not work. If you want to use the async function, you can rewrite it as follows:
useEffect(() => {
(async () => {
// 一些逻辑
})(); // 可返回cleanup函数
}, []);
3) Empty array dependency
Note that useEffect passing an empty array dependency is prone to some problems, which are usually easily overlooked, such as the following example:
function ChildComponent({ count }) {
useEffect(() => {
console.log('componentDidMount', count);
return () => {
// 永远为0,由Capture Value特性所导致
alert('componentWillUnmount and count is ' + count);
};
}, []);
console.log('count', count);
return <>count:{count}</>;
}
const App = () => {
const [count, setCount] = useState(0);
const [childShow, setChild] = useState(true);
return (
<div onClick={() => setCount(c => c + 1)}>
{' '}
<button onClick={() => setChild(false)}>销毁Child组件</button>{' '}
{childShow && <ChildComponent count={count} />}{' '}
</div>
);
};
Click the "Destroy Child component" button, the browser will pop up the "componentWillUnmount and count is 0" prompt box, no matter how many times setCount is called, this will be the case, which is caused by the Capture Value feature. The componentWillUnmount life cycle of the class component can get the latest count value from this.props.count.
When using useEffect, note that it is not completely equivalent to the life cycle of componentDidUpdate, componentWillUnmount, etc., and you should think about useEffect in the form of "side effects" or state synchronization. But this does not mean that it is not recommended to use empty array dependencies, and it needs to be determined based on the context. Rather than treating useEffect as a function to go through 3 separate life cycles, it may be more helpful to simply treat it as a way to run side effects after rendering.
The design intent of useEffect is to pay attention to the changes in the data flow, and then decide how the effect should be executed, which needs to be distinguished from the life cycle thinking model.
useLayoutEffect
React also provides useLayoutEffect, which has the same status as useEffect. Both useEffect and useLayoutEffect can obtain the changed properties of the DOM in side effects:
const App = () => {
const [value, setValue] = useState(0);
useEffect(function useEffectCallBack() {
const nvDom = document.getElementById('content');
console.log('color effect', nvDom.style.color);
});
useLayoutEffect(function useLayoutEffectCallback() {
const nvDom = document.getElementById('content');
console.log('color layout effect', nvDom.style.color);
});
return (
<div
id="content"
style={{ color: value === 1 ? 'red' : '' }}
onClick={() => setValue(c => c + 1)}
>
{' '}
value: {value}{' '}
</div>
);
};
After clicking the button, "color layout effect red" and "color effect red" will be printed. It can be seen that both useEffect and useLayoutEffect can obtain their changed properties from the DOM.
On the surface, there is no difference between useEffect and useLayoutEffect, but in fact, to clarify the difference between them needs to start with the "synchronous" and "asynchronous" side effects. Section 3.2.2 introduced that the operation of useEffect is asynchronous, that is, useEffect does not hinder the rendering of the browser; the difference between useLayoutEffect and useEffect is that the operation of useLayoutEffect is "synchronous", which hinders the rendering of the browser.
In short, useEffect occurs after the browser reflow/repaint operation. If some effects obtain values from the DOM, such as obtaining clientHeight and clientWidth, and need to change the DOM, you can use useLayoutEffect instead to make these operations It is done before the reflow/repaint operation, so that there is a chance to avoid the browser from spending a lot of cost to perform the reflow/repaint operation multiple times. Take an example to illustrate:
const App = () => {
const [value, setValue] = useState(0);
useEffect(function useEffectCallBack() {
console.log('effect');
}); // 在下一帧渲染前执行
window.requestAnimationFrame(() => {
console.log('requestAnimationFrame');
});
useLayoutEffect(function useLayoutEffectCallback() {
console.log('layoutEffect');
});
console.log('render');
return <div onClick={() => setValue(c => c + 1)}>value: {value}</div>;
};
Debug and print in the useEffect, requestAnimationFrame, useLayoutEffect, and render processes to observe their timing. As you can see, when the App is rendered, it will be printed in the following order: render, layoutEffect, requestAnimationFrame, effect. It can be seen that the side effects of useLayoutEffect are all before the "drawing" stage, and the side effects of useEffect are all after the "drawing" stage. Observe the execution of the task through the browser debugging tool, as shown in Figure 3-2. In Figure 3-2, ① executes useLayoutEffectCallback, which is the side effect of useLayoutEffect; ② is the browser's Paint process; after the Paint process, ③ executes the function as useEffectCallBack, which executes the side effect of useEffect.
useRef
When using class components, you usually need to declare attributes to save DOM nodes. With the help of useRef, you can also save the reference of the DOM node in the function component:
import { useRef } from "React"
function App() {
const inputRef = useRef(null);
return<div>
<input type="text" ref={inputRef} />
{/* 通过inputRef.current获取节点 */}
<button onClick={() => inputRef.current.focus()}>focus</button>
</div>
}// useRef的签名为:
interface MutableRefObject<T> {
current: T;
}
function useRef<T>(initialValue: T): MutableRefObject<T>;
useRef returns a variable Ref object whose current property is initialized as the passed parameter (initialValue). The mutable object returned by useRef is like a "box", this "box" exists in the entire life cycle of the component, and its current property holds a mutable value.
useRef is not only suitable for DOM node references, similar to instance attributes on classes, useRef can also be used to store some information that has nothing to do with the UI. For the variable object returned by useRef, its current attribute can hold any value, such as an object, basic type, or function. Therefore, although the functional component has no class instance and no "this", the data storage problem can still be solved through useRef. For example, in section 2.1, useRef has been used:
function Example(props) {
const { history } = props; // 使用useRef保存注销函数
const historyUnBlockCb = React.useRef < UnregisterCallback > (() => {});
React.useEffect(() => {
return () => {
// 在销毁组件时调用,注销history.block
historyUnBlockCb.current();
};
}, []);
function block() {
// 解除之前的阻止
historyUnBlockCb.current();
// 重新设置弹框确认,更新注销函数,单击“确定”按钮,正常跳转;单击“取消”
// 按钮,跳转不生效
historyUnBlockCb.current = history.block('是否继续?');
}
return (
<>
{' '}
<button onClick={block}>阻止跳转</button>{' '}
<button
onClick={() => {
historyUnBlockCb.current();
}}
>
解除阻止
</button>{' '}
</>
);
}
The above example uses useRef to return the variable object historyUnBlockCb, and saves the return value of history.block through historyUnBlockCb.current.
Note that changing the value of refObject.current will not cause re-rendering. If you want to re-render the component, you can use useState, or use some forceUpdate method.
useMemo
As a built-in Hooks of React, useMemo is used to cache the return value of certain functions. useMemo uses a cache to avoid re-executing related functions every time you render. useMemo receives a function and the corresponding dependency array. When a dependency in the dependency array changes, the time-consuming function will be recalculated.
function App() {
const [count, setCount] = React.useState(0);
const forceUpdate = useForceUpdate();
const expensiveCalcCount = count => {
console.log('expensive calc');
let i = 0;
while (i < 9999999) i++;
return count;
}; // 使用useMemo记录高开销的操作
const letterCount = React.useMemo(() => expensiveCalcCount(count), [count]);
console.log('component render');
return (
<div style={{ padding: '15px' }}>
{' '}
<div>{letterCount}</div> <button onClick={() => setCount(c => c + 1)}>改变count</button>{' '}
<button onClick={forceUpdate}>更新</button>{' '}
</div>
);
}
In the above example, in addition to using React.useState, a custom Hook—useForceUpdate is also used, which returns the forceUpdate function, which is consistent with the function of the forceUpdate function in the class component. About custom Hook, it will be introduced in section 3.2.7.
When the App is initially rendered, the function in React.useMemo will be calculated once, and the corresponding count value and the result returned by the function will be recorded by useMemo.
If you click the "Change count" button, due to the change of count, when the App is rendered again, React.useMemo finds that the count has changed, and will re-call expensiveCalcCount and calculate its return value. Therefore, the console will print "expensive calc" and "component render".
If you click the "Update" button, the forceUpdate function is called to render again. Since React.useMemo finds that the count value has not changed during the re-rendering process, it will return the result of the function calculation in React.useMemo last time, and the rendering App console will only print "component render".
At the same time, React also provides useCallback to cache functions:
useCallback(fn, deps)
In terms of implementation, useCallback is equivalent to useMemo(() => fn, deps), so I won’t repeat it here.
useContext
If you want to use the Context described in section 3.1 in the function component, in addition to using Context.Consumer for consumption, you can also use useContext:
const contextValue = useContext(Context);
useContext receives a Context object (return value of React.createContext) and returns the current value of the Context. Similar to Consumer in Section 3.1, the current Context value is provided by the nearest Context.Provider in the upper-level component. When the closest Context.Provider in the upper-level component is updated, the function component that uses useContext will trigger a re-rendering and obtain the latest value passed to the Context.Provider.
Components that call useContext will always be re-rendered when the Context value changes. This feature will be used frequently.
In the function component, using useContext to obtain the context content effectively solves the previous problem that Provider and Consumer need to package additional components, and because it replaces the Context.Consumer's render props writing method, this will make the component tree more concise.
Custom Hook
Custom Hook is a function, and its name convention starts with use, so that it can be seen that this is a Hooks method. If the name of a function starts with use and other hooks are called, it is called a custom hook. Custom Hooks are just like ordinary functions. Arbitrary input and output parameters can be defined. The only thing to note is that custom Hooks need to follow the basic guidelines of Hooks, such as not being used in conditional loops or ordinary functions.
The custom Hook solves the shared logic problem in the previous React components. By customizing Hooks, logic such as form processing, animation, and declaration subscriptions can be abstracted into functions. Custom Hook is a way to reuse logic and is not constrained by internal calls. In fact, each call to Hooks will have a completely isolated state. Therefore, the same custom Hook can be used twice in a component. The following are two examples of commonly used custom Hooks:
// 获取forceUpdate函数的自定义Hook
export default function useForceUpdate() {
const [, dispatch] = useState(Object.create(null));
const memoizedDispatch = useCallback(() => {
// 引用变化
dispatch(Object.create(null));
}, [dispatch]);
return memoizedDispatch;
}
Get the last rendered value of a variable:
// 获取上一次渲染的值
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
Many custom Hooks can be defined based on the basic React Hooks, such as useLocalStorage, useLocation, useHistory (to be introduced in Chapter 5), etc. After abstracting the logic into a custom Hook, the code will be more maintainable.
Refs
createRef
The previous article introduced useRef to save DOM nodes. In fact, you can also create Ref objects through createRef:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
When this.myRef is passed to the div element, the div native node can be obtained in the following ways:
const node = this.myRef.current;
Ref can not only act on DOM nodes, but also on class components. When this property is used on a class component, the current property of the Ref object will obtain an instance of the class component, so the public method of the component instance can also be called.
forwardRef
Ref forwading is a technology that automatically passes reference Ref to sub-components through components. For example, some input components need to control their focus, which could have been controlled by Ref, but because the input has been wrapped in the component, you need to use forwardRef to get the reference of the input through the component.
import React, { Component } from 'react';
import ReactDOM, { render } from 'react-dom';
const ChildOrigin = (props, ref) => {
return <div ref={ref}>{props.txt}</div>;
};
const Child = React.forwardRef(ChildOrigin);
class Parent extends Component {
constructor() {
super();
this.myChild = React.createRef();
}
componentDidMount() {
console.log(this.myChild.current);
// 获取的是Child组件中的div元素
}
render() {
return <Child ref={this.myChild} txt="parent props txt" />;
}
}
After using forwardRef on the original ChildOrigin component to obtain a new Child component, the Ref of the new Child component will be passed to the ChildOrigin component. In the above example, the reference of the div element inside the ChildOrigin component can be obtained through the Ref value this.myChild. current of the new Child component.
Memo
In order to improve the running performance of React, React v16.6.0 provides a high-level component-React.memo. When React.memo wraps a functional component, React will cache the output rendering result, and then will skip this rendering when it encounters the same rendering conditions. Similar to React's PureComponent component, React.memo uses a shallow comparison caching strategy by default, but React.memo corresponds to a functional component, and React.PureComponent corresponds to a class component. The signature of React.memo is as follows:
function memo<P extends object>(
Component: SFC<P>,
propsAreEqual?: (prevProps: Readonly<PropsWithChildren<P>>,
nextProps: Readonly<PropsWithChildren<P>>
) => boolean): NamedExoticComponent<P>;
The first parameter in the React.memo parameter list receives a function component, and the second parameter represents the optional props comparison function. React.memo will return a new memoized component after wrapping the function component. To illustrate with an example, if there is a child component, ChildComponent, which is not memorized by React.memo:
function ChildComponent({ count }) {
console.log('childComponent render', count);
return <>count:{count}</>;
}
const App = () => {
const [count] = useState(0);
const [childShow, setChild] = useState(true);
return (
<div>
{' '}
<button onClick={() => setChild(c => !c)}>隐藏/展示内容</button>{' '}
{childShow && <div>内容</div>} <ChildComponent count={count} />{' '}
</div>
);
};
When the button is clicked repeatedly, the ChildComponent will be updated due to triggering the re-rendering, and "childComponent render" will be printed multiple times. If the React.memo (ChildComponent) cache component is introduced, React will check when the component is rendered. If the props rendered by the component are different from the previously rendered props, React will trigger the rendering; conversely, if the props do not change before and after, React will not perform rendering, and will not perform virtual DOM difference checking. It will use the last rendering result .
function ChildComponent({ count }) {
console.log('childComponent render');
return <>count:{count}</>;
}
const MemoChildComponent = React.memo(ChildComponent);
const App = () => {
const [count] = useState(0);
const [childShow, setChild] = useState(true);
return (
<div>
{' '}
<button onClick={() => setChild(c => !c)}> 隐藏/展示内容</button>{' '}
{childShow && <div>内容</div>} <MemoChildComponent count={count} />{' '}
</div>
);
};
When the "Hide/Show Content" button is clicked, it will cause re-rendering, but since the original component was packaged by React.memo and the packaged component MemoChildComponent was used, the props did not change during multiple renderings, so it will not Print "childComponent render" multiple times.
At the same time, React.memo can use the second parameter propsAreEqual to customize the logic of rendering or not:
const MemoChildComponent = React.memo(ChildComponent, function propsAreEqual(prevProps, nextProps) {
return prevProps.count === nextProps.count;
});
propsAreEqual receives the last prevProps and the nextProps to be rendered. The boolean value returned by the function indicates whether the props before and after are equal. If it returns "true", the props before and after are considered equal; otherwise, it is considered unequal, and React will determine the rendering of the component based on the return value of the function (similar to shouldComponentUpdate). Therefore, it can be considered that the function returns "true" and the props are equal, and rendering is not performed; if the function returns "false", it is considered that the props have changed and React will perform rendering. Note that React.memo cannot be placed in the component rendering process.
const App = () => {
// 每次都获得新的记忆化组件
const MemoChildComponent = React.memo(ChildComponent);
const [count] = useState(0);
const [childShow, setChild] = useState(true);
return (
<div>
{' '}
<button onClick={() => setChild(c => !c)}>隐藏/展示内容</button>{' '}
{childShow && <div>内容</div>} <MemoChildComponent count={count} />{' '}
</div>
);
};
This is equivalent to opening up a new cache for each rendering. The original cache cannot be used, and the memoization of React.memo will be invalid. Developers need to pay special attention.
summary
This chapter introduces React features such as Context, Hooks, Refs, Memo, etc. The above features are all involved in the React Router source code and related third-party library implementations. Mastering the above features is very helpful for understanding React Router and using React Router for actual combat.
Compared with props and state, React's Context feature can achieve cross-level component communication. We can find examples of using Context in many framework designs, and React Router is one of them. Learning to use Context is very important for understanding React Router. At the same time, this chapter introduces React Hooks, as a new feature of React v16.8, and considering the future evolution trend of React Router, learning to use React Hooks for functional component development will be of great help to readers.
references
- https://zh-hans.reactjs.org/docs/context.html.
- https://en.wikipedia.org/wiki/Sideeffect(computer_science).
- https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext.
- https://github.com/facebook/react/blob/v16.8.6/packages/shared/shallowEqual.js.
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#Description.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。