2019 年开始,在使用React的时候,已经逐步从 Class 组件,过渡到函数组件了。虽然函数组件十分便捷,可是在使用函数组件的时候,仍是有一些疑惑的地方,致使有时候会出现一些奇奇怪怪的问题。在这里,我想经过官网和博客文章以及本身的一些积累,整理下最佳实践,以备不时之需。html
Hook 不会影响你对 React 概念的理解。 偏偏相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。稍后咱们将看到,Hook 还提供了一种更强大的方式来组合他们。node
下面是一些官方列举的,提出并使用 hooks 的一些动机。react
你可使用 Hook 从组件中提取状态逻辑,使得这些逻辑能够单独测试并复用。Hook 使你在无需修改组件结构的状况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。git
useEffectgithub
咱们常常维护一些组件,组件起初很简单,可是逐渐会被状态逻辑和反作用充斥。每一个生命周期经常包含一些不相关的逻辑。例如,组件经常在 componentDidMount
和 componentDidUpdate
中获取数据。可是,同一个 componentDidMount
中可能也包含不少其它的逻辑,如设置事件监听,而以后需在 componentWillUnmount
中清除。相互关联且须要对照修改的代码被进行了拆分,而彻底不相关的代码却在同一个方法中组合在一块儿。如此很容易产生 bug,而且致使逻辑不一致。npm
在多数状况下,不可能将组件拆分为更小的粒度,由于状态逻辑无处不在。这也给测试带来了必定挑战。同时,这也是不少人将 React 与状态管理库结合使用的缘由之一。可是,这每每会引入了不少抽象概念,须要你在不一样的文件之间来回切换,使得复用变得更加困难。编程
为了解决这个问题,Hook 将组件中相互关联的部分拆分红更小的函数(好比设置订阅或请求数据),而并不是强制按照生命周期划分。你还可使用 reducer 来管理组件的内部状态,使其更加可预测。json
除了代码复用和代码管理会遇到困难外,咱们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this
的工做方式,这与其余语言存在巨大差别。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码很是冗余。你们能够很好地理解 props,state 和自顶向下的数据流,但对 class 却束手无策。即使在有经验的 React 开发者之间,对于函数组件与 class 组件的差别也存在分歧,甚至还要区分两种组件的使用场景。
为了解决这些问题,Hook 使你在非 class 的状况下可使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。api
useState
是咱们要学习的第一个 “Hook”,它的用途是管理状态。数组
const [state, setState] = useState(initialState);复制代码
返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 (state
) 与传入的第一个参数 (initialState
) 值相同。
setState
函数用于更新 state。它接收一个新的 state 值并将组件的一次从新渲染加入队列。
setState(newState);复制代码
在后续的从新渲染中,useState
返回的第一个值将始终是更新后最新的 state。
注意
React 会确保
setState
函数的标识是稳定的,而且不会在组件从新渲染时发生变化。这就是为何能够安全地从useEffect
或useCallback
的依赖列表中省略setState
。
与 class 组件中的 setState
方法不一样,useState
不会自动合并更新对象。你能够用函数式的 setState
结合展开运算符来达到合并更新对象的效果。
setState(prevState => {
// 也可使用 Object.assign
return {...prevState, ...updatedValues};
});复制代码
useReducer
是另外一种可选方案,它更适合用于管理包含多个子值的 state 对象。
initialState
参数只会在组件的初始渲染中起做用,后续渲染时会被忽略。若是初始 state 须要经过复杂计算得到,则能够传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});复制代码
useEffect
就是一个 Effect Hook,给函数组件增长了操做反作用的能力。它跟 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具备相同的用途,只不过被合并成了一个 API。
当你调用 useEffect
时,就是在告诉 React 在完成对 DOM 的更改后运行你的“反作用”函数。因为反作用函数是在组件内声明的,因此它们能够访问到组件的 props 和 state。默认状况下,React 会在每次渲染后调用反作用函数 —— 包括第一次渲染的时候。
反作用函数还能够经过返回一个函数来指定如何“清除”反作用。可是,这个清除做用,只有当组件被干掉了,才会触发。
有时候,咱们只想在 React 更新 DOM 以后运行一些额外的代码。好比发送网络请求,手动变动 DOM,记录日志,这些都是常见的无需清除的操做。由于咱们在执行完这些操做以后,就能够忽略他们了。
useEffect
作了什么? 经过使用这个 Hook,你能够告诉 React 组件须要在渲染后执行某些操做。React 会保存你传递的函数(咱们将它称之为 “effect”),而且在执行 DOM 更新以后调用它。
为何在组件内部调用 useEffect
? 将 useEffect
放在组件内部让咱们能够在 effect 中直接访问 count
state 变量(或其余 props)。咱们不须要特殊的 API 来读取它 —— 它已经保存在函数做用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的状况下,还引入特定的 React API。
useEffect
会在每次渲染后都执行吗? 是的,默认状况下,它在第一次渲染以后
提示
与 componentDidMount
或 componentDidUpdate
不一样,使用 useEffect
调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数状况下,effect 不须要同步地执行。在个别状况下(例如测量布局),有单独的 useLayoutEffect
Hook 供你使用,其 API 与 useEffect
相同。
以前,咱们研究了如何使用不须要清除的反作用,还有一些反作用是须要清除的。例如订阅外部数据源。这种状况下,清除工做是很是重要的,能够防止引发内存泄露!
为何要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每一个 effect 均可以返回一个清除函数。如此能够将添加和移除订阅的逻辑放在一块儿。它们都属于 effect 的一部分。
React 什么时候清除 effect? React 会在组件卸载的时候执行清除操做。正如以前学到的,effect 在每次渲染的时候都会执行。这就是为何 React
在下面这个 Effect 中,咱们会在每次 props.friend.id
更新的时候,解除以前订阅的函数,并用新的状态 props.friend.id
订阅函数。
并不须要特定的代码来处理更新逻辑,由于 useEffect
在某些状况下,每次渲染后都执行清理或者执行 effect 可能会致使性能问题。在 class 组件中,咱们能够经过在 componentDidUpdate
中添加对 prevProps
或 prevState
的比较逻辑解决:
这是很常见的需求,因此它被内置到了 useEffect
的 Hook API 中。若是某些特定值在两次重渲染之间没有发生变化,你能够通知 React 跳过对 effect 的调用,只要传递数组做为 useEffect
的第二个可选参数便可:
注意:
若是你要使用此优化方式,请确保数组中包含了全部外部做用域中会随时间变化而且在 effect 中使用的变量,不然你的代码会引用到先前渲染中的旧变量。参阅文档,了解更多关于如何处理函数以及数组频繁变化时的措施内容。
若是想执行只运行一次的 effect(仅在组件挂载和卸载时执行),能够传递一个空数组([]
)做为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,因此它永远都不须要重复执行。这并不属于特殊状况 —— 它依然遵循依赖数组的工做方式。
若是你传入了一个空数组([]
),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入 []
做为第二个参数更接近你们更熟悉的 componentDidMount
和 componentWillUnmount
思惟模式,但咱们有更好的方式来避免过于频繁的重复调用 effect。除此以外,请记得 React 会等待浏览器完成画面渲染以后才会延迟调用 useEffect
,所以会使得额外操做很方便。
咱们推荐启用 eslint-plugin-react-hooks
中的 exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
const value = useContext(MyContext);复制代码
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value
prop 决定。
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext
provider 的 context value
值。即便祖先使用 React.memo
或 shouldComponentUpdate
,也会在组件自己使用 useContext
时从新渲染。
别忘记 useContext
的参数必须是
useContext(MyContext)
useContext(MyContext.Consumer)
useContext(MyContext.Provider)
调用了 useContext
的组件总会在 context 值变化时从新渲染。若是重渲染组件的开销较大,你能够 经过使用 memoization 来优化。
提示
若是你在接触 Hook 前已经对 context API 比较熟悉,那应该能够理解,
useContext(MyContext)
至关于 class 组件中的static contextType = MyContext
或者<MyContext.Consumer>
。
useContext(MyContext)
只是让你可以读取context 的值以及订阅 context 的变化。你仍然须要在上层组件树中使用<MyContext.Provider>
来为下层组件提供context。
把以下代码与 Context.Provider 放在一块儿
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> );
}复制代码
对先前 Context 高级指南中的示例使用 hook 进行了修改,你能够在连接中找到有关如何 Context 的更多信息。
const [state, dispatch] = useReducer(reducer, initialArg, init);复制代码
useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。(若是你熟悉 Redux 的话,就已经知道它如何工做了。)
在某些场景下,useReducer
会比 useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于以前的 state 等。而且,使用 useReducer
还能给那些会触发深更新的组件作性能优化,由于你能够向子组件传递 dispatch
而不是回调函数 。
如下是用 reducer 重写 useState
一节的计数器示例:
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>
</>
);
}复制代码
注意
React 会确保
dispatch
函数的标识是稳定的,而且不会在组件从新渲染时改变。这就是为何能够安全地从useEffect
或useCallback
的依赖列表中省略dispatch
。
有两种不一样初始化 useReducer
state 的方式,你能够根据使用场景选择其中的一种。将初始 state 做为第二个参数传入 useReducer
是最简单的方法:
const [state, dispatch] = useReducer(
reducer,
{count: initialCount} );复制代码
注意
React 不使用
state = initialState
这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,所以须要在调用 Hook 时指定。若是你特别喜欢上述的参数约定,能够经过调用useReducer(reducer, undefined, reducer)
来模拟 Redux 的行为,但咱们不鼓励你这么作。
你能够选择惰性地建立初始 state。为此,须要将 init
函数做为 useReducer
的第三个参数传入,这样初始 state 将被设置为 init(initialArg)
。
这么作能够将用于计算 state 的逻辑提取到 reducer 外部,这也为未来对重置 state 的 action 作处理提供了便利:
function init(initialCount) { return {count: initialCount};}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset': return init(action.payload); default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init); return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}复制代码
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);复制代码
返回一个 memoized 回调函数。
把内联回调函数及依赖项数组做为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给通过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将很是有用。
useCallback(fn, deps)
至关于 useMemo(() => fn, deps)
。
注意
依赖项数组不会做为参数传给回调函数。虽然从概念上来讲它表现为:全部回调函数中引用的值都应该出如今依赖项数组中。将来编译器会更加智能,届时自动建立数组将成为可能。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);复制代码
返回一个 memoized 值。
把“建立”函数和依赖项数组做为参数传入 useMemo
,它仅会在某个依赖项改变时才从新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
记住,传入 useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操做,诸如反作用这类的操做属于 useEffect
的适用范畴,而不是 useMemo
。
若是没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。
你能够把 useMemo
做为性能优化的手段,但不要把它当成语义上的保证。未来,React 可能会选择“遗忘”之前的一些 memoized 值,并在下次渲染时从新计算它们,好比为离屏组件释放内存。先编写在没有 useMemo
的状况下也能够执行的代码 —— 以后再在你的代码中添加 useMemo
,以达到优化性能的目的。
注意
依赖项数组不会做为参数传给“建立”函数。虽然从概念上来讲它表现为:全部“建立”函数中引用的值都应该出如今依赖项数组中。将来编译器会更加智能,届时自动建立数组将成为可能。
const refContainer = useRef(initialValue);复制代码
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
一个常见的用例即是命令式地访问子组件:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}复制代码
本质上,useRef
就像是能够在其 .current
属性中保存一个可变值的“盒子”。
你应该熟悉 ref 这一种访问 DOM 的主要方式。若是你将 ref 对象以 <div ref={myRef} />
形式传入组件,则不管该节点如何改变,React 都会将 ref 对象的 .current
属性设置为相应的 DOM 节点。
然而,useRef()
比 ref
属性更有用。它能够很方便地保存任何可变值,其相似于在 class 中使用实例字段的方式。
这是由于它建立的是一个普通 Javascript 对象。而 useRef()
和自建一个 {current: ...}
对象的惟一区别是,useRef
会在每次渲染时返回同一个 ref 对象。
请记住,当 ref 对象内容发生变化时,useRef
并
.current
属性不会引起组件从新渲染。若是想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则须要使用
回调 ref 来实现。
获取 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 的值的
注意到咱们传递了 []
做为 useCallback
的依赖列表。这确保了 ref callback 不会在再次渲染时改变,所以 React 不会在非必要的时候调用它。
本例中,仅在组件挂载和卸载时调用回调ref,由于 <h1>
组件在每次从新渲染时,高度都不会变化,因此不须要触发回调ref。若是你但愿在组件调整大小时获得通知,可使用ResizeObserver或使用其余第三方的Hooks。
若是你愿意,你能够 把这个逻辑抽取出来做为 一个可复用的 Hook:
function MeasureExample() {
const [rect, ref] = useClientRect(); return (
<>
<h1 ref={ref}>Hello, world</h1>
{rect !== null &&
<h2>The above header is {Math.round(rect.height)}px tall</h2>
}
</>
);
}
function useClientRect() {
const [rect, setRect] = useState(null);
const ref = useCallback(node => {
if (node !== null) {
setRect(node.getBoundingClientRect());
}
}, []);
return [rect, ref];
}复制代码
useImperativeHandle(ref, createHandle, [deps])复制代码
useImperativeHandle
可让你在使用 ref
时自定义暴露给父组件的实例值。在大多数状况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与 forwardRef
一块儿使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);复制代码
在本例中,渲染 <FancyInput ref={inputRef} />
的父组件能够调用 inputRef.current.focus()
。
其函数签名与 useEffect
相同,但它会在全部的 DOM 变动以后同步调用 effect。可使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制以前,useLayoutEffect
内部的更新计划将被同步刷新。
尽量使用标准的 useEffect
以免阻塞视觉更新。
提示
若是你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则须要注意
useLayoutEffect
与componentDidMount
、componentDidUpdate
的调用阶段是同样的。可是,咱们推荐你一开始先用useEffect
,只有当它出问题的时候再尝试使用useLayoutEffect
。若是你使用服务端渲染,请记住,
不管useLayoutEffect
仍是useEffect
都没法在 Javascript 代码加载完成以前执行。这就是为何在服务端渲染组件中引入useLayoutEffect
代码时会触发 React 告警。解决这个问题,须要将代码逻辑移至useEffect
中(若是首次渲染不须要这段逻辑的状况下),或是将该组件延迟到客户端渲染完成后再显示(若是直到useLayoutEffect
执行以前 HTML 都显示错乱的状况下)。若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,能够经过使用
showChild && <Child />
进行条件渲染,并使用useEffect(() => { setShowChild(true); }, [])
延迟展现组件。这样,在客户端渲染完成以前,UI 就不会像以前那样显示错乱了。
useDebugValue(value)复制代码
useDebugValue
可用于在 React 开发者工具中显示自定义 hook 的标签。
例如,“自定义 Hook” 章节中描述的名为 useFriendStatus
的自定义 Hook:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}复制代码
其实不只是对象,函数在每次渲染时也是独立的。这就是 Capture Value 特性。
通常来讲,不安全。
function Example({ someProp }) {
function doSomething() {
console.log(someProp); }
useEffect(() => {
doSomething();
}, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)}复制代码
要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为何 一般你会想要在 effect
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp); }
doSomething();
}, [someProp]); // ✅ 安全(咱们的 effect 仅用到了 `someProp`)}复制代码
若是这样以后咱们依然没用到组件做用域中的任何值,就能够安全地把它指定为 []
:
useEffect(() => {
function doSomething() {
console.log('hello');
}
doSomething();
}, []); // ✅ 在这个例子中是安全的,由于咱们没有用到组件做用域中的 *任何* 值复制代码
根据你的用例,下面列举了一些其余的办法。
注意
咱们提供了一个
exhaustive-deps
ESLint 规则做为eslint-plugin-react-hooks
包的一部分。它会帮助你找出没法一致地处理更新的组件。
让咱们来看看这有什么关系。
若是你指定了一个 依赖列表 做为 useEffect
、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
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 内部,这样它就不用出如今它的依赖列表中了。
提示
若是处于某些缘由你
useCallback
Hook。这就确保了它不随渲染而改变,除非
function ProductPage({ productId }) {
// ✅ 用 useCallback 包裹以免随渲染发生改变 const fetchProduct = useCallback(() => { // ... Does something with productId ... }, [productId]); // ✅ useCallback 的全部依赖都被指定了
return <ProductDetails fetchProduct={fetchProduct} />;
}
function ProductDetails({ fetchProduct }) {
useEffect(() => {
fetchProduct();
}, [fetchProduct]); // ✅ useEffect 的全部依赖都被指定了
// ...
}复制代码
注意在上面的案例中,咱们 须要 让函数出如今依赖列表中。这确保了 ProductPage
的 productId
prop 的变化会自动触发 ProductDetails
的从新获取。
注意
咱们推荐 在 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]);
}复制代码
不管如何,咱们都 不推荐使用这种模式 ,只是为了文档的完整性而把它展现在这里。相反的,咱们更倾向于 避免向下深刻传递回调。
利用 useRef
就能够绕过 Capture Value 的特性。能够认为 ref
在全部 Render 过程当中保持着惟一引用,所以全部对 ref
的赋值或取值,拿到的都只有一个最终状态,而不会在每一个 Render 间存在隔离。
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
// ...
}复制代码
也能够简洁的认为,ref
是 Mutable 的,而 state
是 Immutable 的。
上述例子使用了 count
,然而这样的代码很别扭,由于你在一个只想执行一次的 Effect 里依赖了外部变量。
既然要诚实,那只好 想办法不依赖外部变量:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);复制代码
setCount
还有一种函数回调模式,你不须要关心当前值是什么,只要对 “旧的值” 进行修改便可。这样虽然代码永远运行在第一次 Render 中,但老是能够访问到最新的 state
。
将更新与动做解耦就能够了:
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" }); // Instead of setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);复制代码
这就是一个局部 “Redux”,因为更新变成了 dispatch({ type: "tick" })
因此无论更新时须要依赖多少变量,在调用更新的动做里都不须要依赖任何变量。 具体更新操做在 reducer
函数里写就能够了。在线 Demo。
Dan 也将
useReducer
比做 Hooks 的的金手指模式,由于这充分绕过了 Diff 机制,不过确实能解决痛点!
function Parent() {
const [query, setQuery] = useState("react");
// ✅ Preserves identity until query changes
const fetchData = useCallback(() => {
const url = "https://hn.algolia.com/api/v1/search?query=" + query;
// ... Fetch data and return it ...
}, [query]); // ✅ Callback deps are OK
return <Child fetchData={fetchData} />;
}
function Child({ fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // ✅ Effect deps are OK
// ...
}复制代码
因为函数也具备 Capture Value 特性,通过 useCallback
包装过的函数能够看成普通变量做为 useEffect
的依赖。useCallback
作的事情,就是在其依赖变化时,返回一个新的函数引用,触发 useEffect
的依赖变化,并激活其从新执行。
自定义 Hook 是一种天然遵循 Hook 设计的约定,而并非 React 的特性。
自定义 Hook 必须以 “use
” 开头吗?必须如此。这个约定很是重要。不遵循的话,因为没法判断某个函数是否包含对其内部 Hook 的调用,React 将没法自动检查你的 Hook 是否违反了 Hook 的规则。
在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用
自定义 Hook 如何获取独立的 state?每次
useFriendStatus
,从 React 的角度来看,咱们的组件只是调用了
useState
和
useEffect
。 正如咱们在
以前章节中
了解到的同样,咱们能够在一个组件中屡次调用
useState
和
useEffect
,它们是彻底独立的。
因为 Hook 自己就是函数,所以咱们能够在它们之间传递信息。
咱们将使用聊天程序中的另外一个组件来讲明这一点。这是一个聊天消息接收者的选择器,它会显示当前选定的好友是否在线:
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1); const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} /> <select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}复制代码
咱们将当前选择的好友 ID 保存在 recipientID
状态变量中,并在用户从 <select>
中选择其余好友时更新这个 state。
因为 useState
为咱们提供了 recipientID
状态变量的最新值,所以咱们能够将它做为参数传递给自定义的 useFriendStatus
Hook:
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);复制代码
如此可让咱们知道
recipientID
状态变量时,
useFriendStatus
Hook 将会取消订阅以前选中的好友,并订阅新选中的好友状态。
若是你错过自动合并,你能够写一个自定义的 useLegacyState
Hook 来合并对象 state 的更新。然而,咱们推荐把 state 切分红多个 state 变量,每一个变量包含的不一样值会在同时发生变化。
能够 经过 ref 来手动实现:
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。