Hooks FAQ
Hooks 是一项新功能提案,可让你在不编写类的情况下使用状态(state)和其他React功能。它们目前处于React v16.7.0-alpha中,并在此RFC中进行讨论。
此页面回答了一些有关Hooks的常见问题。
采用策略
我是否需要重写所有类组件?
不需要,我们没有计划在React中删除Class——我们都需要保持出货的产品( keep shipping products ),不可能承受重写的成本。我们建议你在新代码中尝试使用Hook。
我的现有React知识中有多少能保持相关性?
Hooks给你提供了一种更加直接使用React相关功能——例如状态,生命周期,上下文和引用的方式。但它们并没有从根本上改变React的工作方式,因此你对组件,Props和自上而下数据流的了解也同样重要。
Hooks确实有自己的学习曲线。如果本文档中缺少某些内容,请提出issue,我们会尽力提供帮助。
我应该使用Hooks,Class还是两者兼而有之?
当你准备好了,我们鼓励你开始尝试在你新组件中使用Hooks。请确保团队中的每个人都使用它们并熟悉本文档。我们不建议将现有Class重写为Hooks,除非你计划重写它们(例如修复bugs)。
你不能在类组件中使用Hooks ,但你绝对可以在一棵组件树中将Class组件和使用Hooks的函数组件混合在一起。无论一个组件是Class还是使用Hook的函数,这只是该组件的实现细节。当然从长远来看,我们希望Hooks成为人们编写React组件的主要方式。
Hooks涵盖了Class的所有用例吗?
我们的目标是让Hooks尽快涵盖Class的所有用例。对于不常见getSnapshotBeforeUpdate
和componentDidCatch
生命周期目前还没有对应的Hook,但我们计划会很快添加上。
对于Hook来说,现在还是一个非常早的时期,因此一些方面的集成(如DevTools支持,或Flow/TypeScript类型)可能还没有准备好。某些第三方库也可能与Hook不兼容。
Hooks会替换render props和高阶组件吗?
通常,render props和高阶组件只渲染一个children。我们认为使用Hooks实现这种用例将会更加简单。就目前而言,这两种模式仍然有其立足之地(例如,虚拟滚动组件可能具有renderItem
prop,或者可视容器组件可能具有其自己的DOM结构)。但在大多数情况下,使用Hooks就足够了,它可以帮助你减少组件树中的嵌套。
connect()
和React Router等)?
Hook对流行框架的API来说意味着什么(Redux的首先,你还可以继续像以往一样使用这些API,没有任何影响。(毕竟函数组件和Class组件本质上没太多区别)
其次,这些库的新版本也可能导出自定义Hook,例如,useRedux()
或者useRouter()
允许你使用相同的功能而不需要包装器组件。
Hooks可以使用静态类型吗?
Hooks的设计考虑了静态类型。因为它们是函数,所以它们比高阶组件之类的模式更容易正确键入。我们已提前与Flow和TypeScript团队联系,他们计划在未来包含React Hooks的定义。
重要的是,如果你想以某种方式更严格地键入React API,则可以考虑使用自定义Hook,它可以让你有权限制React API。React为你提供了原语,但你可以采用与我们提供的开箱即用方式所不同的方式将它们组合在一起。
如何测试使用Hooks的组件?
从React的角度来看,使用Hooks的组件也只是一个普通的组件。如果你的测试解决方案不依赖于React内部,则测试使用了Hooks的组件应与你正常测试组件的方式相同。
如果你需要测试自定义Hook,可以通过在测试中创建一个组件并使用自定义Hook来实现。然后,你就可以测试你所编写的组件。
lint规则究竟强制执行了什么?
我们提供了一个ESLint插件,它强制执行Hooks规则以避免错误。它假设任何以“ use
” 开头的函数和紧跟在它之后的大写字母是一个Hook。我们认识到这种启发式方法并不完美,可能存在一些误报,但如果没有整个生态系统的约定,就没有办法让Hooks良好的运作 —— 更长的名字会阻止人们采用Hooks或遵循其惯例。
特别是,该规则强制执行:
- 对Hooks的调用要么在Pascal命名法(PascalCase)的函数内部(假设是一个组件),要么是另一个
useSomething
函数(假定为自定义Hook)。 - 在每个渲染上以相同的顺序调用Hook。
还有一些启发式方法,它们可能会随着时间的推移而改变,因为我们会对规则进行微调以寻求在发现错误和避免误报之间的平衡。
从 Classes 过渡到 Hooks
Class中的生命周期与Hook的对应情况
constructor
:函数组件不需要构造函数。你可以通过调用useState
进行初始化。如果计算成本很高,你可以传递一个函数给useState
。getDerivedStateFromProps
:改为在渲染时安排更新。shouldComponentUpdate
:通过React.memo
,下文会介绍render
:就是函数本身。componentDidMount
,componentDidUpdate
,componentWillUnmount
:useEffect
Hook可表示所有这些组合(包括不怎么常见 、常见用例)。componentDidCatch
andgetDerivedStateFromError
: 暂无,后续会加上。
是否有类似实例变量的东西?
有的! useRef()
Hook不只是可以用在DOM上。“ref”对象实际上是一个通用容器,其current
属性是可变的,可以保存任何值,类似于类上的实例属性。
你可以从useEffect
从修改它:
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
}
如果我们只是想设置一个间隔,我们就不需要ref(id
可以作为effect的local变量),但如果我们想从事件处理程序中清除间隔,它会很有用:
// ...
function handleCancelClick() {
clearInterval(intervalRef.current);
}
// ...
从概念上讲,你可以将refs视为类中的实例变量。但是,请避免在渲染过程中设置引用 —— 这可能会导致出乎意料的行为。相反,你应该只在事件处理程序和Effect中的修改引用。
我应该使用一个还是多个状态变量?
如果你来自Class模式,你可能总是想要在useState()
一次调用的时就候将所有状态放入一个对象中。如果你愿意,你可以这样做。以下是鼠标移动后的组件示例。我们在local保持其position和size:
function Box() {
const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
// ...
}
现在我们想写当用户移动鼠标的的时候,改变left
以及top
的逻辑。请注意,我们必须手动将这些字段合并到以前的状态对象中:
// ...
useEffect(() => {
function handleWindowMouseMove(e) {
// Spreading "...state" ensures we don't "lose" width and height
setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
}
// Note: this implementation is a bit simplified
window.addEventListener('mousemove', handleWindowMouseMove);
return () => window.removeEventListener('mousemove', handleWindowMouseMove);
}, []);
// ...
这是因为当我们更新状态变量时,我们会替换它的值。这是不同于this.setState
的一点,它会自动合并了更新的字段到对象。
如果你怀念自动合并的方式,则可以编写自动合并对象状态更新的自定义HookuseLegacyState
。但是,我们建议根据哪些值趋于一同更改将状态拆分为多个状态变量。
例如,我们可以将组件状态拆分为position
和size
对象,并始终替换position
而不需要合并
function Box() {
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
useEffect(() => {
function handleWindowMouseMove(e) {
setPosition({ left: e.pageX, top: e.pageY });
}
// ...
分离独立的状态变量也有另一个好处。稍后可以轻松地将一些相关逻辑提取到自定义Hook中,例如:
function Box() {
const position = useWindowPosition();
const [size, setSize] = useState({ width: 100, height: 100 });
// ...
}
function useWindowPosition() {
const [position, setPosition] = useState({ left: 0, top: 0 });
useEffect(() => {
// ...
}, []);
return position;
}
请注意我们如何在不更改代码的情况下,将与position
状态变量相关的useStateh
和Effect移动到自定义Hook中。如果所有状态都在单个对象中,提取它们将更加困难。
将所有状态都放在一次useState
调用中,亦或是将每个字段都使用一次useState
调用,这两种方式都行的通。当你能在这两个极端之间找到平衡,将组相关状态分组为几个独立的状态变量时,组件往往最具可读性。如果状态逻辑变得复杂,我们建议用reducer的方式或自定义Hook 管理它。
我可以仅在更新时运行Effect吗?
这是一个罕见的用例。如果需要,可以使用手动操作ref的方式,手动存储一个布尔值,该值对应于你是第一次还是后续渲染做判断,然后在Effect中检查该标志。(如果你发现自己经常这样做,可以为它创建一个自定义Hook。)
如何获得以前的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,因为它是一个相对常见的用例。
另请参见派生状态的推荐模式。
getDerivedStateFromProps
?
我该如何实现虽然你可能不需要它,但在极少数情况下(例如实现<Transition>
组件),你可以在渲染期间更新状态。在退出第一个渲染后,React将立即重新运行具有更新状态的组件,因此它不会很昂贵。
在这里,我们将row
prop 的先前值存储在状态变量中,以便我们可以比较:
function ScrollView({row}) {
let [isScrollingDown, setIsScrollingDown] = useState(false);
let [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
这看起来可能很奇怪,但其渲染过程中的更新过程正是与getDerivedStateFromProps
在概念上一致的。
我可以对函数组件进行引用吗?
虽然你不应需要经常这样做,但你可以通过使用useImperativeMethods
Hook 向父组件暴露一些命令性方法。
const [thing, setThing] = useState()
是什么意思?
如果你不熟悉这个语法,可以查看State Hook文档中的这个解释。
性能优化
我可以在更新的时候跳过一个effect吗?
是。请参阅有条件地触发Effect。请注意,忘记处理更新通常会引入错误,这就是为什么这不是一个默认行为。
shouldComponentUpdate
?
我该如何实现你可以用React.memo
包装一个函数组件,进而来浅显比较它的props:
const Button = React.memo((props) => {
// your component
});
它不是一个Hook,因为它不像Hooks那样构成。React.memo
相当于PureComponent
,但它只比较props。(你还可以添加第二个参数来指定采用旧props和新props的自定义比较函数。如果它返回true,则跳过更新。)
React.memo
不比较状态,因为没有单个状态对象可以进行比较。但是你也可以让children变得纯粹(pure),甚至可以通过useMemo
优化个别children。
如何记忆计算?
useMemo
Hook就可以让你在多次渲染的时候,缓存之前的计算结果
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
这段代码会调用computeExpensiveValue(a, b)
。但是如果[a, b]
自上一个值以来一直没有改变,则useMemo
会跳过第二次调用它并简单地重用它返回的最后一个值。
方便的是,它也允许你跳过重渲染一个代价昂贵的child:
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
。
由于在渲染中创建函数,Hooks是否会变慢?
答案是否定的,在现代浏览器中,除了极端情况之外,与类相比,使用闭包的原始性能并没有显着差异。
此外,考虑到Hooks的设计在以下几个方面更有效:
- Hooks避免了类所需的大量开销,例如在构造函数中创建类实例和绑定(binding)事件处理程序的成本。
- 使用Hooks的惯用代码不需要深层组件树嵌套,而这种嵌套在使用高阶组件,render props和Context的代码库中很常见。使用较小的组件树,React的工作量也会较少。
传统上,React中内联函数的性能问题与每次渲染上传递新的回调会中断子组件中的shouldComponentUpdate
优化有关。Hooks从三个方面解决了这个问题。
useCallback
Hook 可以让你在重渲染的时候依然保持对同一回调的引用,这样shouldComponentUpdate
就能继续工作:
// Will not change unless `a` or `b` changes
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
- 当个别children更新时,通过使用
useMemo
Hook使得它更容易控制,同时也减少了对pure components的需求。 - 最后,
useReducer
Hook减少了深度传递回调的需要,接下来会介绍。
如何避免传递回调?
我们发现大多数人不喜欢手动在组件树的每一层进行回调的传递。虽然它更明确,但它可能感觉做了很多“脏活累活(plumping)”。
在大型组件树中,我们建议的另一种方法是通过从context的useReducer
Hook 中传递一个dispatch
函数:
const TodosDispatch = React.createContext(null);
function TodosApp() {
// Tip: `dispatch` won't change between re-renders
const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
}
TodosApp
树里面的任何一个孩子都可以使用dispatch
函数传递action到TodosApp
:
function DeepChild(props) {
// If we want to perform an action, we can get dispatch from context.
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
<button onClick={handleClick}>Add todo</button>
);
}
从维护的角度来看这更方便(不需要保持转发回调),并且完全避免了回调问题。在深度更新dispatch
像这样向下传递是深度更新的推荐模式。
请注意,你仍然可以选择是将应用程序状态作为props(更明确)或是作为上下文传递(对于非常深的更新更方便)。如果你同时也使用上下文传递状态,请使用不同的上下文类型 —— dispatch
的上下文永远不会更改,因此读取它的组件不需要重新渲染,除非它们还需要应用程序状态。
useCallback
读取经常变化的值?
如何从Also note that this pattern might cause problems in the concurrent mode. We plan to provide more ergonomic alternatives in the future, but the safest solution right now is to always invalidate the callback if some value it depends on changes.
注意
我们建议从Context向下传递
dispatch
而不是在props中传单个回调。下面的方法仅在此处提及只是为了完整性和预留逃生舱口(escape hatch)。另请注意,此模式可能会导致并发模式出现问题。我们计划在未来提供更符合人体工程学的替代方案,但现在最安全的解决方案是,如果某些值依赖于更改,则始终使回调无效。
在极少数情况下,你可能需要使用useCallback
去memoize一个回调,但是因为内部函数必须经常重新创建,因此memoization不能很好地工作。如果你要记忆的函数是事件处理程序并且它在渲染期间并未使用,则可以使用ref作为实例变量,并将最后提交的值手动保存到其中:
function Form() {
const [text, updateText] = useState('');
const textRef = useRef();
useMutationEffect(() => {
textRef.current = text; // Write it to the ref
});
const handleSubmit = useCallback(() => {
const currentText = textRef.current; // Read it from the ref
alert(currentText);
}, [textRef]); // Don't recreate handleSubmit like [text] would do
return (
<>
<input value={text} onChange={e => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} />
</>
);
}
这是一个相当复杂的模式,它表明如果你需要的话你依然可以执行此逃逸舱口优化(escape hatch optimization)。当然,如果将其提取到自定义Hook就会更好点:
function Form() {
const [text, updateText] = useState('');
// Will be memoized even if `text` changes:
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.');
});
useMutationEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
在任何一种情况下,我们都不建议使用此模式,仅在此处显示完整性。相反,你最好避免向深处传递回调。
底层实现(Under the Hood)
React如何将Hook调用与组件相关联?
React跟踪当前渲染组件。由于Hooks规则,我们知道Hook只能从React组件(或自定义Hooks调用 ——它们也只能从React组件中调用)。
每个组件都有一个与之相关联的“存储器单元(memory cells)”的内部列表(list)。它们只是一些可以放置一些数据的JavaScript对象。当你调用Hook时useState()
,它会读取当前单元格(或在第一次渲染期间初始化它),然后将指针移动到下一个单元格。这就是多个useState()
调用各自获得独立本地状态的方式。可以参考
Hooks的现有技术是什么?
Hooks综合了几个不同来源的想法:
- 我们旧的实验性的功能API在react-future仓库中。
- 与render props API 相关的React社区的实验,包括Ryan Florence的Reactions Component。
- Dominic Gannaway提出了一个render props糖语法的
adopt
关键字提案。 - DisplayScript中的状态变量和状态单元( state cells )。
- ReasonReact中的Reducer组件。
- Rx中的Subscriptions。
- 多核OCaml中的代数效应(Algebraic effects)。
SebastianMarkbåge提出了Hooks的原创设计,后来由Andrew Clark,Sophie Alpert,Dominic Gannaway以及React团队的其他成员完善。