# React Hooks
# Hooks 简介
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。具体而言,分为以下 Hook
- 基础 Hook
- 额外的 Hook
# 基础 Hook
# useState
const [state, setState] = useState(initialState);
返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 ( state
) 与传入的第一个参数 ( initialState
) 值相同。
setState
函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
setState(newState);
//or
setState((value)=>{
...
return newState;
})
在后续的重新渲染中, useState
返回的第一个值将始终是更新后最新的 state。
# 函数式更新
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState
。该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了 setState
的两种用法:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
“+” 和 “-” 按钮采用函数式形式,因为被更新的 state 需要基于之前的 state。但是 “重置” 按钮则采用普通形式,因为它总是把 count 设置回初始值。
如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
# 惰性初始 state
initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
# useEffect
useEffect(didUpdate);
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
首先说一下什么是副作用,一个纯函数应该只存在输入到输出的映射,如果有其他的作用,便可称之为副作用。
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志、ajax 操作、 等等都是副作用 **(异步操作、延迟操作都算)**,以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect
完成副作用操作。赋值给 useEffect
的函数会在组件渲染到屏幕之后执行。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。
# 清除 effect
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点, useEffect
函数需返回一个清除函数。以下就是一个创建订阅的例子:
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。在上述示例中,意味着组件的每一次更新都会创建新的订阅。若想避免每次更新都触发 effect 的执行,请参阅下一小节。
# effect 的执行时机
与 componentDidMount
、 componentDidUpdate
不同的是,传给 useEffect
的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。
然而,并非所有 effect 都可以被延迟执行。例如,一个对用户可见的 DOM 变更就必须在浏览器执行下一次绘制前被同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect
Hook 来处理这类 effect。它和 useEffect
的结构相同,区别只是调用时机不同。
虽然 useEffect
会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。在开始新的更新前,React 总会先清除上一轮渲染的 effect。
# effect 的条件执行
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在 source
prop 改变时重新创建。
要实现这一点,可以给 useEffect
传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
此时,只有当 props.source
改变后才会重新创建订阅。
注意
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。请参阅文档,了解更多关于如何处理函数 以及数组频繁变化时的措施 的内容。
** 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组(
[]
)作为第二个参数。** 这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。如果你传入了一个空数组(
[]
),effect 内部的 props 和 state 就会一直持有其初始值。尽管传入[]
作为第二个参数有点类似于componentDidMount
和componentWillUnmount
的思维模式,但我们有 更好的 方式 来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用useEffect
,因此会使得处理额外操作很方便。
依赖项数组不会作为参数传给 effect 函数。虽然从概念上来说它表现为:所有 effect 函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
# useContext
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
的参数必须是 context 对象本身:
- 正确:
useContext(MyContext)
- 错误:
useContext(MyContext.Consumer)
- 错误:
useContext(MyContext.Provider)
调用了 useContext
的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。
把如下代码与 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> );
}
# 额外的 Hook
以下介绍的 Hook,有些是上一节中基础 Hook 的变体,有些则仅在特殊情况下会用到。不用特意预先学习它们。
# useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。(
在某些场景下, 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
。
# 指定初始 state
有两种不同初始化 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>
</>
);
}
# 跳过 dispatch
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is
比较算法 来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的 “深层” 节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo
来进行优化。
# 通过 useContext 和 useReducer 实现的一种状态管理
考虑如下一种情况,有一个书架组件和阅读组件,书架组件可以上传本地书籍,阅读组件可以阅读上传的本地书籍。在书籍没有上传到后台的情况下,如何实现两组件间的通信 (书籍信息共享) 呢?
首先先考虑数据挂载在哪?很明显应该挂在两组件的父组件上 (不考虑 redux 时)。那么既然如此,通过 props 的逐层传递是一种解决方案,但是比较麻烦。更为简便的方案就是通过 useContext 和 useReducer 来实现。
我们可以同通过在其父组件上挂载一个 Context
export const BookListContext = React.createContext();
const reducer = (state, action) => {
switch (action.type) {
case "addBooks":
return handleAddBooks(state, action.value);
case "deleteBooks":
return handleDeleteBooks(state, action.value);
default:
return state;
}
};
<BookListContext.Provider
value={{
bookList: bookList,
bookListDispatch: dispatch,
}}
>
<Routes>
<Route path="/book/:bookName" element={<Book />}></Route>
<Route path="/" element={<Bookshelf />}></Route>
</Routes>
</BookListContext.Provider>
子组件需要时,进行调用
const bookListContext = useContext(BookListContext);
const bookList=bookListContext.bookList;
bookListContext.bookListDispatch({type:'addBooks',value:fileLists});
bookListContext.bookListDispatch({type:'deleteBooks',value:bookName});
# useRef
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 来实现。