React 16.8 的新特性 —— React Hooks

Hooks are a new addition in React 16.8.
They let you use state and other React features without writing a class.

背景

  • 在组件之间复用状态逻辑很难 (render props 和 高阶组件容易造成嵌套地狱)
  • 部分复杂的组件中,相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起(例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。)
  • 难以理解的class (this、bind等)

使用规则

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。

推荐配置 eslint-plugin-react-hooks 来检测语法

Hooks 介绍

下面介绍一下比较常用的几个API

  • useState

const [count, setCount] = useState(0);

useState函数接受一个参数,为该state的初始值(可以传入函数计算得出初始值),返回一个数组,利用数组解构的语法,可得到该state以及修改该state的函数。
setCount可以setCount(newCount),也可以setCount(prev => prev + 1),注意它与setState的合并是不一样的,它是完全替换的。

React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的。所以一定要记得:不要在循环、条件判断或者子函数中调用Hook。

  • useEffect

1
2
3
4
5
6
7
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
subscription.unsubscribe();
};
});

数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。这些操作应该放在useEffect中。在组件每次渲染之后才会调用useEffect。useEffect 会在浏览器绘制后延迟执行。
useEffect有清除机制,在return中的函数,React会在执行清除操作时调用它。
useEffect(() => {} , [deps]) 仅当依赖deps变化时才执行函数,若为空数组则只执行一次。
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。配置上文提到的eslint插件后,可检测出deps是否有遗漏。

PS: React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 setState。

  • useContext

const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。

  • useReducer

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数。

React官网关于useReducer的说明

  • useCallback / useMemo

1
2
3
4
5
6
7
8
9
10
11
12
// useCallback
// 把内联回调函数及依赖项数组作为参数传入useCallback,它将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

// useMemo
// 把“创建”函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算memoized值。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

有些人可能会误以为 useCallback 可以用来解决创建函数造成的性能问题,其实恰恰相反,单从这个组件看的话 useCallback 只会更慢,因为 inline 函数是无论如何都会创建的,还增加了 useCallback 内部对依赖变化的检测。
useCallback 的真正目的还是在于缓存了每次渲染时 inline callback 的实例,这样方便配合上子组件的 shouldComponentUpdate 或者 React.memo 起到减少不必要的渲染的作用。需要注意的是,在优化子组件的性能时,React.memo 和 React.useCallback 一定记得需要配对使用,缺了一个都可能导致性能不升反“降”
( React.memo是函数式组件的PureComponet实现,详情 > React.memo介绍 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用了useCallback,性能更差
function CandyDispenser() {
const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
const [candies, setCandies] = React.useState(initialCandies)
// const dispense = candy => {
// setCandies(allCandies => allCandies.filter(c => c !== candy))
// }
const dispense = React.useCallback(candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}, [])
return (
<div>
<p>{candies}</p>
<button onClick={dispense} />
</div>
)
}

So when should I useMemo and useCallback?
There are specific reasons both of these hooks are built-into React:

  • Referential equality
  • Computationally expensive calculations

当涉及到引用类型的比较(适用useCallback和useMemo),或需要昂贵的计算时(适用useMemo),我们才考虑使用它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 在useEffect判断依赖时,保持引用的一致
function Blub() {
const bar = React.useCallback(() => {}, [])
const baz = React.useMemo(() => [1, 2, 3], [])
React.useEffect(() => {
const options = {bar, baz}
console.log(options)
}, [bar, baz])
}

// 避免了CountButton的不必要的re-renders
const CountButton = React.memo(function CountButton({onClick, count}) {
return <button onClick={onClick}>{count}</button>
})
function DualCounter() {
const [count1, setCount1] = React.useState(0)
const increment1 = React.useCallback(() => setCount1(c => c + 1), [])
const [count2, setCount2] = React.useState(0)
const increment2 = React.useCallback(() => setCount2(c => c + 1), [])
return (
<div>
<CountButton count={count1} onClick={increment1} />
<CountButton count={count2} onClick={increment2} />
</div>
)
}
1
2
3
4
5
6
7
8
// 避免了重复的昂贵计算
function RenderPrimes({iterations, multiplier}) {
const primes = React.useMemo(() => calculatePrimes(iterations, multiplier), [
iterations,
multiplier,
])
return <div>Primes! {primes}</div>
}
  • useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

  • useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect 以避免阻塞视觉更新。

自定义Hook

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

假设我们有几个组件都需要做权限校验,我们就可以把权限校验的逻辑提取出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 自定义Hook
function usePermission(pageName: string) {
const [permission, setPermission] = useState(false);

useEffect(() => {
// 异步请求权限
fetchPermission(pageName).then(res => {
setPermission(res)
})
}, [pageName]);

return permission;
}

function Home() {
const permission = usePermission('Home')

useEffect(() => {
if (!!permission) {
console.log('I have the permission to operate this page')
}
}, [permission])

return (
<div>
<p>Home</p>
{
permission &&
(<button onClick={handleClick}>更新</button>)
}
</div>
)
}

现在我们已经把这个权限校验逻辑提取到 usePermission 的自定义 Hook 中,然后就可以在所有页面组件中使用它了。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头,这样可以一眼看出其符合 Hook 的规则。

一般来说,自定义Hook会在内部维护自己的state,当函数参数发生变化或者执行了某种异步操作后,通过useEffect去动态改变state的值,最后返回该state。这样,外部调用的组件就可以根据这个state来获取所需要的信息。

  • 在两个组件中使用相同的 Hook 会共享 state 吗?
    不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

  • 提起出自定义Hook的代码等价于原来的代码吗?
    等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。

自定义 Hook 解决了以前在 React 组件中无法灵活共享逻辑的问题。你可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器,甚至可能还有其他我们没想到的场景。更重要的是,创建自定义 Hook 就像使用 React 内置的功能一样简单。
(一个自定义Hook的优秀例子:Debouncing with React Hooks)