动机(官方)
- 组件之间很难重用有状态逻辑
- 复杂的组件变得难以理解
- 类 class 混淆了人和机器
- 更符合 FP 的理解, React 组件本身的定位就是函数,一个吃进数据、吐出 UI 的函数
基础hook
useState
const [state, setState] = useState(initialState)
- useState 有一个参数,该参数可以为任意数据类型,一般用作默认值
- useState 返回值为一个数组,数组的第一个参数为我们需要使用的 state,第二个参数为一个 setFn。
- 完整例子
function Love() {
const [like, setLike] = useState(false)
const likeFn = () => (newLike) => setLike(newLike)
return (
<>
你喜欢我吗: {like ? 'yes' : 'no'}
<button onClick={likeFn(true)}>喜欢</button>
<button onClick={likeFn(false)}>不喜欢</button>
</>
)
}
注意:
如果初始值是个函数;取得是他的返回值;而不是函数本身
- 调用 useState 方法的时候做了什么?
这是一种在函数调用时保存变量的方式 —— useState是一种新方法,它与 class 里面的 this.state提供的功能完全相同。
一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
我们声明了一个叫 count 的 state 变量,然后把它设为 0。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用 setCount 来更新当前的 count。
useEffect
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。
然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。
如果我的 effect 的依赖频繁变化,我该怎么办?
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>;
}
(setState 函数的身份是被确保稳定的,所以可以放心地在依赖项中省略掉)
额外的Hooks
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
}, [a, b], );
返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
注意
依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
为了性能优化而生:
不必要每个函数都用useCallback 包一下;说得很清楚;
1.函数执行只代表执行了render;如果两个虚拟dom没有差异;就不会更新dom, 不代表操作了dom元素。
2.明确说了传递给经过优化并使用引用相等性去避免非必要渲染的子组件时,他将非常有用,啥意思用memo包裹一下子组件,方法就用useCallback,属性就用useMemo
场合memo一起用:
function PageA(props:any) {
const { onClick, children } = props
console.log('a---render ')
return <TouchableOpacity onPress={onClick}>
<Text>{children}</Text>
</TouchableOpacity>
}
function PageB ({ onClick, name }) {
console.log('b----render')
useEffect(()=>{
console.log('b Mounted')
return ()=>{ }
},[])
return <TouchableOpacity onPress={onClick}>
<Text>{name}</Text>
</TouchableOpacity>
}
const PageC = memo(PageB)
function UseCallback() {
const [a, setA] = useState(0)
const [b, setB] = useState(0)
const handleClick1 = () => {
setA(a + 1)
}
// const handleClick2 =() => {
// setB(b + 1)
// }
const handleClick2 = useCallback(() => {
setB(b + 1)
}, [b])
return (
<>
<PageA onClick={handleClick1}>{a}</PageA>
<PageC onClick={handleClick2} name={b} />
</>
)
}
memo与PureComponent比较类似,前者是对Function Component的优化,后者是对Class Component的优化,都会对传入组件的数据进行浅比较,memo缓存的是组件本身,是站在全局的角度进行优化
const handleClick2 = useCallback(() => { setB(b + 1) }, [b])
useCallback 则是对函数的缓存,依赖项b不变化;则handleCick 不必变化;就应该缓存起来,提高性能,减少对资源的浪费
需不需要每个函数都是用useCallback
它的目的是为了一些子组件不必要的重新渲染。
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。
注意
依赖项数组不会作为参数传给“创建”函数。虽然从概念上来说它表现为:所有“创建”函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
我们推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。
注意:
useMemo 缓存的结果是回调函数中return回来的值,主要用于缓存计算结果的值,应用场景如需要计算的状态
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>
</>
);
}
//或者:
function Image(props) {
// ?? IntersectionObserver 在每次渲染都会被创建
const ref = useRef(new IntersectionObserver(onIntersect));
// ...
}
function Image(props) {
const ref = useRef(null);
// ? IntersectionObserver 只会被惰性创建一次
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.current;
}
// 当你需要时,调用 getObserver()
// ...
}
本质上,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 来实现。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
另外 useReducer 可以让你通过 reducer 来管理组件本地的复杂 state。
function Todos() {
const [todos, dispatch] = useReducer(todosReducer);
// ...
例如,有个复杂的组件,其中包含了大量以特殊的方式来管理的内部状态。useState 并不会使得集中更新逻辑变得容易,因此你可能更愿意使用 redux 中的 reducer 来编写、
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { text: action.text, completed: false }];
// ... other actions ...
default:
return state;
} }
Reducers 非常便于单独测试,且易于扩展,以表达复杂的更新逻辑。如有必要,您可以将它们分成更小的 reducer。但是,你可能还享受着 React 内部 state 带来的好处,或者可能根本不想安装其他库。Reducers 非常便于单独测试,且易于扩展,以表达复杂的更新逻辑。如有必要,您可以将它们分成更小的 reducer。但是,你可能还享受着 React 内部 state 带来的好处,或者可能根本不想安装其他库。
那么,为什么我们不编写一个 useReducer 的 Hook,使用 reducer 的方式来管理组件的内部 state 呢?其简化版本可能如下所示:
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
在组件中使用它,让 reducer 驱动它管理 state:
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);
function handleAddClick(text) {
dispatch({ type: 'add', text });
}
// ...
}
在复杂组件中使用 reducer 管理内部 state 的需求很常见,我们已经将 useReducer 的 Hook 内置到 React 中。你可以在 Hook API 索引中找到它使用,搭配其他内置的 Hook 一起使用
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
注意
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 来进行优化。
useImperativeHandle
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()。
注意:current时useRef多加了一层;如果你用函数的方式接受;就没有current这一层;
useLayoutEffect
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
提示
如果你正在将代码重写 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffect 与 componentDidMount、componentDidUpdate 的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect。
如果你使用服务端渲染,请记住,无论如何 useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。解决这个问题,需要将代码逻辑转移至 useEffect 中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect 执行之前 HTML 都是显示错乱的情况下)。
若要从服务端渲染的 HTML 布局排除依赖布局 effect 的组件,可以通过使用 showChild && <Child /> 进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, []) 延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。
Hooks FAQ
向外暴露方法给父组件使用
推荐:
useImperativeHandle 向外暴露api
function AddAbnormalGoods (props:ISProps,ref:any):React.ReactElement {
useImperativeHandle(ref,()=>({
componentWillAppear(){
removeOnPlayCompletionSubscript = audioRecordUtils.onPlayCompletion(() => {
setIsPlaying(false)
})
},
componentWillDisappear(){
if (removeOnPlayCompletionSubscript) {
removeOnPlayCompletionSubscript()
removeOnPlayCompletionSubscript = null
}
stopRecord()
},
props
})
)
}
有类似实例变量的东西吗?
function Timer() {
//可以存储一些跨声明周期的 数据;渲染前后数据不变
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
}
function AddAbnormalGoods (props:ISProps,ref:any):React.ReactElement {
const instance = useRef<ISRefInstance>({
removeOnPlayCompletionSubscript:null,
timer:null,
voiceImg:null,
animatedView:null
})
//可以结构出来
let {removeOnPlayCompletionSubscript,timer,voiceImg,animatedView} = instance.current
}
推荐实践:
组件定义
Function Component 采用 const + 箭头函数方式定义:
const App: React.FC<{ title: string }> = ({ title }) => {
return React.useMemo(() => <div>{title}</div>, [title]);
};
App.defaultProps = {
title: 'Function Component'
}
上面的例子包含了:
- 用 React.FC 申明 Function Component 组件类型与定义 Props 参数类型。
- 用 React.useMemo 优化渲染性能。
- 用 App.defaultProps 定义 Props 的默认值。
React.FC 只能有一个入参;如果用到forwardRef,就不行了
FAQ
为什么不用解构方式代替 defaultProps?
虽然解构方式书写 defaultProps 更优雅,但存在一个硬伤:对于对象类型每次 Rerender 时引用都会变化,这会带来性能问题,因此不要这么做。
局部状态
局部状态有三种,根据常用程度依次排列: useState useRef useReducer 。
useState
const [hide, setHide] = React.useState(false);
const [name, setName] = React.useState('BI');
状态函数名要表意,尽量聚集在一起申明,方便查阅。
useRef
const dom = React.useRef(null);
useRef 尽量少用,大量 Mutable 的数据会影响代码的可维护性。
但对于不需重复初始化的对象推荐使用 useRef 存储,比如 new G2() 。
useReducer
局部状态不推荐使用 useReducer ,会导致函数内部状态过于复杂,难以阅读。 useReducer 建议在多组件间通信时,结合 useContext 一起使用。
FAQ
可以在函数内直接申明普通常量或普通函数吗?
不可以,Function Component 每次渲染都会重新执行,常量推荐放到函数外层避免性能问题,函数推荐使用 useCallback 申明。
函数
所有 Function Component 内函数必须用 React.useCallback 包裹,以保证准确性与性能。
const [hide, setHide] = React.useState(false);
const handleClick = React.useCallback(() => { setHide(isHide => !isHide) }, [])
useCallback 第二个参数必须写,eslint-plugin-react-hooks 插件会自动填写依赖项。
不过;官网也明确说了;对于传给经过优化,并使用引用相等性的子组件非常有效memo,不必每个都传,本身缓存也有一定开销