提供给函数组件使用的state与setState的方式
hook 是提供一种约定的方式,解决组件共享状态(复用状态)的问题,可以写一个自定义的hook,然后在其他组件中引入使用,解决“理解你的应用状态变化是很困难”的问题,以及“复用状态逻辑”
最初能解决状态复用的途径有2种,
render props
高阶组件
的方式,现在多了一种hook的方式了解决副作用的问题,你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。可以通过useEffect处理副作用的问题
-
[useState](https://zh-hans.reactjs.org/docs/hooks-reference.html#usestate)
[useEffect](https://zh-hans.reactjs.org/docs/hooks-reference.html#useeffect)
[useContext](https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext)
- 额外的 Hook
[useReducer](https://zh-hans.reactjs.org/docs/hooks-reference.html#usereducer)
[useCallback](https://zh-hans.reactjs.org/docs/hooks-reference.html#usecallback)
[useMemo](https://zh-hans.reactjs.org/docs/hooks-reference.html#usememo)
[useRef](https://zh-hans.reactjs.org/docs/hooks-reference.html#useref)
[useImperativeHandle](https://zh-hans.reactjs.org/docs/hooks-reference.html#useimperativehandle)
[useLayoutEffect](https://zh-hans.reactjs.org/docs/hooks-reference.html#uselayouteffect)
[useDebugValue](https://zh-hans.reactjs.org/docs/hooks-reference.html#usedebugvalue)
useState
为函数组件没有state以及setState,提供useState(a: any)返回一个的数组,0下标为state,1下标为可以变更此state的方法,无状态组件从此变成了有状态组件了,下面是一个例子,其中setCount可以接受一个常量值,也可以接收一个函数
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
initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
useEffect
- useEffect(didUpdate:function[,[a:any]]),第一个形参传入一个函数,它会在每次状态变更后执行,并且可以在传入的函数中返回一个函数作为componentWillUnmount的处理(react 会在销毁时自动执行此返回的函数)。第二个形参可以接收一个数组可以做useEffect优化处理,只有当数组里面具体的值发生变化时,才执行useEffect操作
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 来实现。
useImperativeHandle [imperative 英 [ɪmˈperətɪv]]
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={fancyInputRef} />
的父组件可以调用 fancyInputRef.current.focus()
。
useContext
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value
prop 决定。
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext
provider 的 context value
值。
别忘记 useContext
的参数必须是 context 对象本身:
- 正确:
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。
useReduces
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: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
useCallback 这玩意其实就是依赖缓存更新的意思,和vue的computed感觉一样,当依赖发生改变,它就会被执行
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回一个 memoized 回调函数。
(memoization 是一种优化技术,主要用于通过存储昂贵的函数调用的结果来加速计算机程序,并在再次发生相同的输入时返回缓存的结果。)
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
注意 依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。 我们推荐启用
[eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks#installation)
中的[exhaustive-deps](https://github.com/facebook/react/issues/14920)
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
useMemo 同上意思一样
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值。
useLayoutEffect
其函数签名与 useEffect
相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。官方推荐你一开始先用 **useEffect**
,只有当它出问题的时候再尝试使用 useLayoutEffect
。
useDebugValue
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;
}
提示 我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。
延迟格式化 debug 值
在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。
因此,useDebugValue
接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。
例如,一个返回 Date
值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString
函数调用:
useDebugValue(date, date => date.toDateString());
注意:hook使用规范
- 只能在最顶层使用hook,不能嵌套使用
// ------------
// 首次渲染
// ------------
useState('Mary') // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm) // 2. 添加 effect 以保存 form 操作
useState('Poppins') // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle) // 4. 添加 effect 以更新标题
// -------------
// 二次渲染
// -------------
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm) // 2. 替换保存 form 的 effect
useState('Poppins') // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle) // 4. 替换更新标题的 effect
// ...
// 在条件语句中使用 Hook 违反第一条规则
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
上面21行到25行的代码会导致,Hook 的调用顺序发生了改变:
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm) // 🔴 此 Hook 被忽略!
useState('Poppins') // 🔴 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle) // 🔴 3 (之前为 4)。替换更新标题的 effect 失败
React 不知道第二个 useState 的 Hook 应该返回什么。
React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应得是 persistForm 的 effect,
但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。
- 只能在React函数组件中使用hook
- ESLint检测
我们发布了一个名为 [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks)
的 ESLint 插件来强制执行这两条规则。如果你想尝试一下,可以将此插件添加到你的项目中:
npm install eslint-plugin-react-hooks --save-dev
// 你的 ESLint 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
}
}
- 自定义的hook必须以use开头,已达到能被eslint检测的目的