React 笔记— Hooks

本文基于官方 v18.1.0 文档。

Introducing Hooks

They let you use state and other React features without writing a class.

Motivation

It’s hard to reuse stateful logic between components

现在可以通过自定义 Hook 提取封装带状态的逻辑。

Complex components become hard to understand

useEffect 可以把复杂逻辑分拆到多个小的函数中。

Classes confuse both people and machines

Hooks at a Glance

State Hook

解构赋值语法

React assumes that if you call useState many times, you do it in the same order during every render.

Effect Hook

It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes, but unified into a single API.

Rules of Hooks

  • Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions.
  • Only call Hooks from React function components. Don’t call Hooks from regular JavaScript functions.

Building Your Own Hooks

The state of each component is completely independent. Hooks are a way to reuse stateful logic, not state itself. In fact, each call to a Hook has a completely isolated state — so you can even use the same custom Hook twice in one component.

Custom Hooks are more of a convention than a feature. If a function’s name starts with ”use” and it calls other Hooks, we say it is a custom Hook.

Using the State Hook

Using the Effect Hook

you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.

Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.

Experienced JavaScript developers might notice that the function passed to useEffect is going to be different on every render. This is intentional….Every time we re-render, we schedule a different effect, replacing the previous one.

Unlike componentDidMount or componentDidUpdate, effects scheduled with useEffect don’t block the browser from updating the screen.

React performs the cleanup when the component unmounts….React also cleans up effects from the previous render before running the effects next time.

Tip: Optimizing Performance by Skipping Effects

If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.

Rules of Hooks

Only Call Hooks at the Top Level

Don’t call Hooks inside loops, conditions, or nested functions.

Only Call Hooks from React Functions

Don’t call Hooks from regular JavaScript functions.

Explanation

So how does React know which state corresponds to which useState call? The answer is that React relies on the order in which Hooks are called.

Building Your Own Hooks

A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.

Hooks API Reference

useState

Lazy initial state

If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render.

Batching of state updates

Before React 18, only updates inside React event handlers were batched. Starting with React 18, batching is enabled for all updates by default.

In the rare case that you need to force the DOM update to be applied synchronously, you may wrap it in flushSync. However, this can hurt performance so do this only where needed.

1
2
3
4
5
// Force this state update to be synchronous.
flushSync(() => {
setCount(count + 1);
});
// By this point, DOM is updated.

useEffect

…the previous effect is cleaned up before executing the next effect.

Timing of effects

…the function passed to useEffect fires after layout and paint, during a deferred event.

就是前边说的,effects scheduled with useEffect don’t block the browser from updating the screen. useEffect 不阻塞屏幕刷新。

However, not all effects can be deferred. For example, a DOM mutation that is visible to the user must fire synchronously before the next paint so that the user does not perceive a visual inconsistency.

之前的解决方案就是 useLayoutEffect,18+ 又多了上边提到的 flushSync

Conditionally firing an effect

…make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.

useContext

1
const value = useContext(MyContext);

Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest <MyContext.Provider> above the calling component in the tree.

useContext(MyContext) only lets you read the context and subscribe to its changes. You still need a <MyContext.Provider> above in the tree to provide the value for this context.

useReducer

1
const [state, dispatch] = useReducer(reducer, initialArg, init);

An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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>
</>
);
}

Lazy initialization

…you can pass an init function as the third argument. The initial state will be set to init(initialArg).

useCallback

1
2
3
4
5
6
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed.

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

useMemo

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo will only recompute the memoized value when one of the dependencies has changed.

If no array is provided, a new value will be computed on every render.

useRef

useRef() is useful for more than the ref attribute.

useRef 实际就是返回了一个 js object,只不过生命周期是持续整个组件生命周期的,所以除了用于 ref,任何其它需要的场景都可以用。

useImperativeHandle

1
useImperativeHandle(ref, createHandle, [deps])

自定义 ref.current 的值。createHandle 的结果对象作为 ref.current 的新值。

useImperativeHandle should be used with forwardRef.

useLayoutEffect

useEffect 作用,但是触发时机、对 DOM 的更新都是同步的、阻塞的。

useDebugValue

useDebugValue can be used to display a label for custom hooks in React DevTools.

useDeferredValue

1
const deferredValue = useDeferredValue(value);

value 记忆化。碰到 urgent 更新时,会用旧值渲染;非 urgent 时,以新值渲染。跟防抖不同,useDeferredValue 保证最后一定会以新值渲染一次。所以 useDeferredValue 有可能会触发两次渲染。

细节参考:https://github.com/reactwg/react-18/discussions/129

useTransition

1
const [isPending, startTransition] = useTransition();

Returns a stateful value for the pending state of the transition, and a function to start it.

Updates in a transition yield to more urgent updates such as clicks.

将一个函数调用变为转场。回调函数会至少延迟一个渲染周期后被调用。React 把更新分成两类:Urgent 和 Transition。转场是可以 pending 的,startTransition 被调用到回调函数被调用前,isPendingtrue

细节参考:https://github.com/reactwg/react-18/discussions/41

useId

生成一个唯一字符串。

Hooks FAQ

How can I measure a DOM node?

元素大小不变的情况下不需要 ResizeObserver,用 callback ref。妙啊!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 function MeasureExample() {
const [height, setHeight] = useState(0);

const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);

return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}

How do I implement shouldComponentUpdate?

You can wrap a function component with React.memo to shallowly compare its props:

1
2
3
 const Button = React.memo((props) => {
// your component
});

How to memoize calculations?

The useMemo Hook lets you cache calculations between multiple renders by “remembering” the previous computation:

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Conveniently, useMemo also lets you skip an expensive re-render of a child:

1
2
3
4
5
6
7
8
9
10
11
12
 function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}

How to create expensive objects lazily?

1
2
3
4
5
 function Table(props) {
// ⚠️ createRows() is called on every render
const [rows, setRows] = useState(createRows(props.count));
// ...
}

To avoid re-creating the ignored initial state, we can pass a function to useState:

1
2
3
4
5
 function Table(props) {
// ✅ createRows() is only called once
const [rows, setRows] = useState(() => createRows(props.count));
// ...
}

评论