1、Hook 会替代 render props 和高阶组件吗?
通常,render props 和高阶组件只渲染一个子节点。我们认为让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderItem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。
2、Hook 能否覆盖 class 的所有使用场景?
我们给 Hook 设定的目标是尽早覆盖 class 的所有使用场景。目前暂时还没有对应不常用的 getSnapshotBeforeUpdate,getDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法,但我们计划尽早把它们加进来。
3、Hook 能和静态类型一起用吗?
Hook 在设计阶段就考虑了静态类型的问题。因为它们是函数,所以它们比像高阶组件这样的模式更易于设定正确的类型。最新版的 Flow 和 TypeScript React 定义已经包含了对 React Hook 的支持。
重要的是,在你需要严格限制类型的时候,自定义 Hook 能够帮你限制 React 的 API。React 只是给你提供了基础功能,具体怎么用就是你自己的事了。
4、生命周期方法要如何对应到 Hook?
constructor:函数组件不需要构造函数。你可以通过调用useState来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给useState。getDerivedStateFromProps:改为 在渲染时 安排一次更新。shouldComponentUpdate:详见 下方React.memo.render:这是函数组件体本身。componentDidMount,componentDidUpdate,componentWillUnmount:useEffectHook 可以表达所有这些(包括 不那么 常见 的场景)的组合。getSnapshotBeforeUpdate,componentDidCatch以及getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会被添加。
5、hooks中有类似实例变量的东西吗?
有!useRef() Hook 不仅可以用于 DOM refs。ref 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。
6、怎么替代 shouldComponentUpdate
说实话,Function Component 替代 shouldComponentUpdate 的方案并没有 Class Component 优雅,代码是这样的:
const Button = React.memo(props => {
// your component
});
或者在父级就直接生成一个自带 memo 的子元素:
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}
</>
);
}
相比之下,Class Component 的写法通常是:
class Button extends React.PureComponent {}
7、我应该使用单个还是多个 state 变量?
这个得看使用场景。
useState 目前的一种实践,是将变量名打平,而非像 Class Component 一样写在一个 State 对象里:
class ClassComponent extends React.PureComponent {
state = {
left: 0,
top: 0,
width: 100,
height: 100
};
}
// VS
function FunctionComponent {
const [left,setLeft] = useState(0)
const [top,setTop] = useState(0)
const [width,setWidth] = useState(100)
const [height,setHeight] = useState(100)
}
在函数组件中也可以这样写:
function FunctionComponent() {
const [state, setState] = useState({
left: 0,
top: 0,
width: 100,
height: 100
});
}
但这种写法很明显不太好,state里面的值如果过多的话,就不太容易维护。但是state的粒度也不是越细越好,比如一般left和top是相关的,width和height是相关可以分成两个state。
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
把独立的 state 变量拆分开还有另外的好处。这使得后期把一些相关的逻辑抽取到一个自定义 Hook 变得容易
8、我可以只在更新时运行 effect 吗?
这是个比较罕见的使用场景。如果你需要的话,你可以 使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在你的 effect 中检查这个标识。(如果你发现自己经常在这么做,你可以为之创建一个自定义 Hook。)
9、如何获取上一轮的 props 或 state?
目前,你可以 通过 ref 来手动实现:
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return <h1>Now: {count}, before: {prevCount}</h1>;
}
这或许有一点错综复杂,但你可以把它抽取成一个自定义 Hook:
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <h1>Now: {count}, before: {prevCount}</h1>;
}
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
注意看这是如何作用于 props, state,或任何其他计算出来的值的
function Counter() {
const [count, setCount] = useState(0);
const calculation = count + 100;
const prevCalculation = usePrevious(calculation);
// ...
考虑到这是一个相对常见的使用场景,很可能在未来 React 会自带一个 usePrevious Hook。
10、如何实现 getDerivedStateFromProps?
尽管你可能 不需要它,但在一些罕见的你需要用到的场景下(比如实现一个 <Transition> 组件),你可以在渲染过程中更新 state 。React 会立即退出第一次渲染并用更新后的 state 重新运行组件以避免耗费太多性能。
这里我们把 row prop 上一轮的值存在一个 state 变量中以便比较:
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row 自上次渲染以来发生过改变。更新 isScrollingDown。
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
初看这或许有点奇怪,但渲染期间的一次更新恰恰就是 getDerivedStateFromProps 一直以来的概念。
11、实现forceUpdate
如果前后两次的值相同,useState 和 useReducer Hook 都会放弃更新。原地修改 state 并调用 setState 不会引起重新渲染。
通常,你不应该在 React 中修改本地 state。然而,作为一条出路,你可以用一个增长的计数器来在 state 没变的时候依然强制一次重新渲染:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
12、如何测量DOM节点?
获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。这里有一个 小 demo:
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>
</>
);
}
在这个案例中,我们没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。
注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。
在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用,因为渲染的 <h1> 组件在整个重新渲染期间始终存在。如果你希望在每次组件调整大小时都收到通知,则可能需要使用 ResizeObserver 或基于其构建的第三方 Hook。
可以抽离出来做一个自定义的hook
function useClientRect() {
const [rect, setRect] = useState(null);
const ref = useCallback(node => {
if (node !== null) {
setRect(node.getBoundingClientRect());
}
}, []);
return [rect, ref];
}
13、在依赖列表中省略函数是否安全?
不安全。
function Example({ someProp }) {
function doSomething() {
console.log(someProp);
}
useEffect(() => {
doSomething();
}, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}
要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect *内部* 去声明它所需要的函数。 这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:
如果这样之后我们依然没用到组件作用域中的任何值,就可以安全地把它指定为 []:
如果你指定了一个 依赖列表 作为 useEffect、useLayoutEffect、useMemo、useCallback 或 useImperativeHandle 的最后一个参数,它必须包含回调中的所有值,并参与 React 数据流。这就包括 props、state,以及任何由它们衍生而来的东西。
只有当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。下面这个案例有一个 Bug:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId); // 使用了 productId prop
const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // 🔴 这样是无效的,因为 `fetchProduct` 使用了 `productId`
// ...
}
推荐的修复方案是把那个函数移动到你的 effect *内部*。这样就能很容易的看出来你的 effect 使用了哪些 props 和 state,并确保它们都被声明了:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId
// ...
}
这同时也允许你通过 effect 内部的局部变量来处理无序的响应:
useEffect(() => {
let ignore = false;
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
if (!ignore) setProduct(json);
}
fetchProduct();
return () => { ignore = true };
}, [productId]);
如果处于某些原因你 *无法* 把一个函数移动到 effect 内部,还有一些其他办法:
- 你可以尝试把那个函数移动到你的组件之外。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了。
- 如果你所调用的方法是一个纯计算,并且可以在渲染时调用,你可以 转而在 effect 之外调用它, 并让 effect 依赖于它的返回值。
- 万不得已的情况下,你可以 把函数加入 effect 的依赖但 *把它的定义包裹* 进
useCallbackHook。这就确保了它不随渲染而改变,除非 它自身 的依赖发生了改变
function ProductPage({ productId }) {
// ✅ 用 useCallback 包裹以避免随渲染发生改变
const fetchProduct = useCallback(() => {
// ... Does something with productId ...
}, [productId]); // ✅ useCallback 的所有依赖都被指定了
return <ProductDetails fetchProduct={fetchProduct} />;
}
function ProductDetails({ fetchProduct }) {
useEffect(() => {
fetchProduct();
}, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了
// ...
}
14、如果 effect 的依赖频繁变化,我该怎么办?
有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` 没有被指定为依赖
return <h1>{count}</h1>;
}
传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1。
指定 [count] 作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量
}, 1000);
return () => clearInterval(id);
}, []); // ✅ 我们的 effect 不适用组件作用域中的任何变量
return <h1>{count}</h1>;
}
(setCount 函数的身份是被确保稳定的,所以可以放心的省略掉)
此时,setInterval 的回调依旧每秒调用一次,但每次 setCount 内部的回调取到的 count 是最新值(在回调中变量命名为 c)。
在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),尝试用 useReducer Hook 把 state 更新逻辑移到 effect 之外。这篇文章 提供了一个你该如何做到这一点的案例。 useReducer 的 dispatch 的身份永远是稳定的 —— 即使 reducer 函数是定义在组件内部并且依赖 props。
万不得已的情况下,如果你想要类似 class 中的 this 的功能,你可以 使用一个 ref 来保存一个可变的变量。然后你就可以对它进行读写了。举个例子:
function Example(props) {
// 把最新的 props 保存在一个 ref 中
const latestProps = useRef(props);
useEffect(() => {
latestProps.current = props;
});
useEffect(() => {
function tick() {
// 在任何时候读取最新的 props
console.log(latestProps.current);
}
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []); // 这个 effect 从不会重新执行
}
15、如何实现 shouldComponentUpdate
你可以用 React.memo 包裹一个组件来对它的 props 进行浅比较:
const Button = React.memo((props) => {
// 你的组件
});
这不是一个 Hook 因为它的写法和 Hook 不同。React.memo 等效于 PureComponent,但它只比较 props。(你也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新。)
React.memo 不比较 state,因为没有单一的 state 对象可供比较。但你也可以让子节点变为纯组件,或者 用 useMemo 优化每一个具体的子节点。
16、如何记忆计算结果?
useMemo Hook 允许你通过「记住」上一次计算结果的方式在多次渲染的之间缓存计算结果:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
这行代码会调用 computeExpensiveValue(a, b)。但如果依赖数组 [a, b] 自上次赋值以来没有改变过,useMemo 会跳过二次调用,只是简单复用它上一次返回的值
记住,传给 useMemo 的函数是在渲染期间运行的。不要在其中做任何你通常不会在渲染期间做的事。举个例子,副作用属于 useEffect,而不是 useMemo
**你可以把 useMemo 作为一种性能优化的手段,但不要把它当做一种语义上的保证。**未来,React 可能会选择「忘掉」一些之前记住的值并在下一次渲染时重新计算它们,比如为离屏组件释放内存。建议自己编写相关代码以便没有 useMemo 也能正常工作 —— 然后把它加入性能优化。(在某些取值必须 从不 被重新计算的罕见场景,你可以 惰性初始化 一个 ref。)
方便起见,useMemo 也允许你跳过一次子节点的昂贵的重新渲染:
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}
</>
)
}
注意这种方式在循环中是无效的,因为 Hook 调用 不能 被放在循环中。但你可以为列表项抽取一个单独的组件,并在其中调用 useMemo。
17、如何惰性创建昂贵的对象?
如果依赖数组的值相同,useMemo 允许你记住一次昂贵的计算。但是,这仅作为一种提示,并不 保证 计算不会重新运行。但有时候需要确保一个对象仅被创建一次。
第一个常见的使用场景是当创建初始 state 很昂贵时:
function Table(props) {
// ⚠️ createRows() 每次渲染都会被调用
const [rows, setRows] = useState(createRows(props.count));
// ...
}
为避免重新创建被忽略的初始 state,我们可以传一个 函数 给 useState:
function Table(props) {
// ✅ createRows() 只会被调用一次
const [rows, setRows] = useState(() => createRows(props.count));
// ...
}
React 只会在首次渲染时调用这个函数.**你或许也会偶尔想要避免重新创建 useRef() 的初始值。**举个例子,或许你想确保某些命令式的 class 实例只被创建一次:
function Image(props) {
// ⚠️ IntersectionObserver 在每次渲染都会被创建
const ref = useRef(new IntersectionObserver(onIntersect));
// ...
}
useRef 不会 像 useState 那样接受一个特殊的函数重载。相反,你可以编写你自己的函数来创建并将其设为惰性的
function Image(props) {
const ref = useRef(null);
// ✅ IntersectionObserver 只会被惰性创建一次
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.current;
}
// 当你需要时,调用 getObserver()
// ...
}
这避免了我们在一个对象被首次真正需要之前就创建它。如果你使用 Flow 或 TypeScript,你还可以为了方便给 getObserver() 一个不可为 null 的类型。
18、Hook 会因为在渲染时创建函数而变慢吗?
不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
除此之外,可以认为 Hook 的设计在某些方面更加高效:
- Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。
- 符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。
传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关。Hook 从三个方面解决了这个问题。
-
useCallbackHook 允许你在重新渲染之间保持对相同的回调引用以使得shouldComponentUpdate继续工作:// 除非 `a` 或 `b` 改变,否则不会变 const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); -
useMemoHook 使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要。 -
最后,
useReducerHook 减少了对深层传递回调的依赖,正如下面解释的那样。
19、如何避免层层向下传递回调?
大部分人并不喜欢在组件树的每一层手动传递回调。尽管这种写法更明确,但这给人感觉像错综复杂的管道工程一样麻烦。
在大型的组件树中,我们推荐的替代方案是通过 context 用 useReducer 往下传一个 dispatch 函数:
const TodosDispatch = React.createContext(null);
function TodosApp() {
// 提示:`dispatch` 不会在重新渲染之间变化
const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
}
TodosApp 内部组件树里的任何子节点都可以使用 dispatch 函数来向上传递 actions 到 TodosApp:
function DeepChild(props) {
// 如果我们想要执行一个 action,我们可以从 context 中获取 dispatch。
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
<button onClick={handleClick}>Add todo</button>
);
}
总而言之,从维护的角度来这样看更加方便(不用不断转发回调),同时也避免了回调的问题。像这样向下传递 dispatch 是处理深度更新的推荐模式。
注意,你依然可以选择是要把应用的 state 作为 props 向下传递(更显明确)还是作为作为 context(对很深的更新而言更加方便)。如果你也使用 context 来向下传递 state,请使用两种不同的 context 类型 —— dispatch context 永远不会变,因此组件通过读取它就不需要重新渲染了,除非它们还需要应用的 state。
20、如何从 useCallback 读取一个经常变化的值?
注意:
我们推荐 在 context 中向下传递 dispatch 而非在 props 中使用独立的回调。下面的方法仅仅出于文档完整性考虑,以及作为一条出路在此提及。
同时也请注意这种模式在 并行模式 下可能会导致一些问题。我们计划在未来提供一个更加合理的替代方案,但当下最安全的解决方案是,如果回调所依赖的值变化了,总是让回调失效。
在某些罕见场景中,你可能会需要用 useCallback 记住一个回调,但由于内部函数必须经常重新创建,记忆效果不是很好。如果你想要记住的函数是一个事件处理器并且在渲染期间没有被用到,你可以 把 ref 当做实例变量 来用,并手动把最后提交的值保存在它当中:
function Form() {
const [text, updateText] = useState('');
const textRef = useRef();
useEffect(() => {
textRef.current = text; // 把它写入 ref
});
const handleSubmit = useCallback(() => {
const currentText = textRef.current; // 从 ref 读取它
alert(currentText);
}, [textRef]); // 不要像 [text] 那样重新创建 handleSubmit
return (
<>
<input value={text} onChange={e => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} />
</>
);
}
这是一个比较麻烦的模式,但这表示如果你需要的话你可以用这条出路进行优化。如果你把它抽取成一个自定义 Hook 的话会更加好受些:
function Form() {
const [text, updateText] = useState('');
// 即便 `text` 变了也会被记住:
const handleSubmit = useEventCallback(() => {
alert(text);
}, [text]);
return (
<>
<input value={text} onChange={e => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} />
</>
);
}
function useEventCallback(fn, dependencies) {
const ref = useRef(() => {
throw new Error('Cannot call an event handler while rendering.');
});
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
21、为什么我会在我的函数中看到陈旧的 props 和 state ?
组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。例如,考虑这样的代码:
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
如果你先点击「Show alert」然后增加计数器的计数,那这个 alert 会显示在你点击『Show alert』按钮时的 count 变量。这避免了那些因为假设 props 和 state 没有改变的代码引起问题。
如果你刻意地想要从某些异步回调中读取 最新的 state,你可以用 一个 ref 来保存它,修改它,并从中读取。
最后,你看到陈旧的 props 和 state 的另一个可能的原因,是你使用了「依赖数组」优化但没有正确地指定所有的依赖。举个例子,如果一个 effect 指定了 [] 作为第二个参数,但在内部读取了 someProp,它会一直「看到」 someProp 的初始值。解决办法是要么移除依赖数组,要么修正它。 这里介绍了 你该如何处理函数,而这里介绍了关于如何减少 effect 的运行而不必错误的跳过依赖的 一些常见策略。
478

被折叠的 条评论
为什么被折叠?



