@@ -0,0 +1,290 @@
## 简介
React 16.8 于 2019.2 正式发布,这是一个能提升代码质量和开发效率的特性,笔者就抛砖引玉先列出一些实践点,希望得到大家进一步讨论。
然而需要理解的是,没有一个完美的最佳实践规范,对一个高效团队来说,稳定的规范比合理的规范更重要,因此这套方案只是最佳实践之一。
## 精读
### 环境要求
- 拥有较为稳定且理解函数式编程的前端团队。
- 开启 ESLint 插件:[eslint-plugin-react-hooks ](https://www.npmjs.com/package/eslint-plugin-react-hooks )。
### 组件定义
Function Component 采用 `const` + 箭头函数方式定义:
``` tsx
const App : React.FC < { title : string } > = ( { title } ) = > {
return React . useMemo ( ( ) = > < div > { title } < / div > , [ title ] ) ;
} ;
App . defaultProps = {
title : 'Function Component'
}
```
上面的例子包含了:
1. 用 `React.FC` 申明 Function Component 组件类型与定义 Props 参数类型。
2. 用 `React.useMemo` 优化渲染性能。
3. 用 `App.defaultProps` 定义 Props 的默认值。
#### FAQ
> 为什么不用 React.memo?
推荐使用 `React.useMemo` 而不是 `React.memo` ,因为在组件通信时存在 `React.useContext` 的用法,这种用法会使所有用到的组件重渲染,只有 `React.useMemo` 能处理这种场景的按需渲染。
> 没有性能问题的组件也要使用 useMemo 吗?
要,考虑未来维护这个组件的时候,随时可能会通过 `useContext` 等注入一些数据,这时候谁会想起来添加 `useMemo` 呢?
> 为什么不用解构方式代替 defaultProps?
虽然解构方式书写 `defaultProps` 更优雅,但存在一个硬伤:对于对象类型每次 Rerender 时引用都会变化,这会带来性能问题,因此不要这么做。
### 局部状态
局部状态有三种,根据常用程度依次排列: `useState` `useRef` `useReducer` 。
#### useState
``` tsx
const [ hide , setHide ] = React . useState ( false ) ;
const [ name , setName ] = React . useState ( 'BI' ) ;
```
状态函数名要表意,尽量聚集在一起申明,方便查阅。
#### useRef
``` tsx
const dom = React . useRef ( null ) ;
```
`useRef` 尽量少用,大量 Mutable 的数据会影响代码的可维护性。
但对于不需重复初始化的对象推荐使用 `useRef` 存储,比如 `new G2()` 。
#### useReducer
局部状态不推荐使用 `useReducer` ,会导致函数内部状态过于复杂,难以阅读。 `useReducer` 建议在多组件间通信时,结合 `useContext` 一起使用。
#### FAQ
> 可以在函数内直接申明普通常量或普通函数吗?
不可以,Function Component 每次渲染都会重新执行,常量推荐放到函数外层避免性能问题,函数推荐使用 `useCallback` 申明。
### 函数
所有 Function Component 内函数必须用 `React.useCallback` 包裹,以保证准确性与性能。
``` tsx
const [ hide , setHide ] = React . useState ( false ) ;
const handleClick = React . useCallback ( ( ) = > {
setHide ( isHide = > ! isHide )
} , [ ] )
```
`useCallback` 第二个参数必须写,[eslint-plugin-react-hooks ](https://www.npmjs.com/package/eslint-plugin-react-hooks ) 插件会自动填写依赖项。
### 发请求
发请求分为操作型发请求与渲染型发请求。
#### 操作型发请求
操作型发请求,作为回调函数:
``` tsx
return React . useMemo ( ( ) = > {
return (
< div onClick = { requestService . addList } / >
)
} , [ requestService . addList ] )
```
#### 渲染型发请求
渲染型发请求在 `useAsync` 中进行,比如刷新列表页,获取基础信息,或者进行搜索, **都可以抽象为依赖了某些变量,当这些变量变化时要重新取数 ** :
``` tsx
const { loading , error , value } = useAsync ( async ( ) = > {
return requestService . freshList ( id ) ;
} , [ requestService . freshList , id ] ) ;
```
### 组件间通信
简单的组件间通信使用透传 Props 变量的方式,而频繁组件间通信使用 `React.useContext` 。
以一个复杂大组件为例,如果组件内部拆分了很多模块, **但需要共享很多内部状态 ** ,最佳实践如下:
#### 定义组件内共享状态 - store.ts
``` tsx
export const StoreContext = React . createContext < {
state : State ;
dispatch : React.Dispatch < Action > ;
} > ( null )
export interface State { } ;
export interface Action { type : 'xxx' } | { type : 'yyy' } ;
export const initState : State = { } ;
export const reducer : React.Reducer < State , Action > = ( state , action ) = > {
switch ( action . type ) {
default :
return state ;
}
} ;
```
#### 根组件注入共享状态 - main.ts
``` tsx
import { StoreContext , reducer , initState } from './store'
const AppProvider : React.FC = props = > {
const [ state , dispatch ] = React . useReducer ( reducer , initState ) ;
return React . useMemo ( ( ) = > (
< StoreContext.Provider value = { { state , dispatch } } >
< App / >
< / StoreContext.Provider >
) , [ state , dispatch ] )
} ;
```
#### 任意子组件访问/修改共享状态 - child.ts
``` tsx
import { StoreContext } from './store'
const app : React.FC = ( ) = > {
const { state , dispatch } = React . useContext ( StoreContext ) ;
return React . useMemo ( ( ) = > (
< div > { state . name } < / div >
) , [ state . name ] )
} ;
```
如上解决了 **多个联系紧密组件模块间便捷共享状态的问题 ** ,但有时也会遇到需要共享根组件 Props 的问题,**这种不可修改的状态不适合一并塞到 `StoreContext` 里**,我们新建一个 `PropsContext` 注入根组件的 Props:
``` tsx
const PropsContext = React . createContext < Props > ( null )
const AppProvider : React.FC < Props > = props = > {
return React . useMemo ( ( ) = > (
< PropsContext.Provider value = { props } >
< App / >
< / PropsContext.Provider >
) , [ props ] )
} ;
```
#### 结合项目数据流
参考 [react-redux hooks ](https://github.com/reduxjs/react-redux/blob/master/docs/api/hooks.md )。
### debounce 优化
比如当输入框频繁输入时,为了保证页面流畅,我们会选择在 `onChange` 时进行 `debounce` 。然而在 Function Component 领域中,我们有更优雅的方式实现。
> 其实在 Input 组件 `onChange` 使用 `debounce` 有一个问题,就是当 Input 组件 **受控** 时, `debounce` 的值不能及时回填,导致甚至无法输入的问题。
我们站在 Function Component 思维模式下思考这个问题:
1. React [scheduling ](https://github.com/dt-fe/weekly/blob/v2/099.%E7%B2%BE%E8%AF%BB%E3%80%8AScheduling%20in%20React%E3%80%8B.md ) 通过智能调度系统优化渲染优先级,我们其实不用担心频繁变更状态会导致性能问题。
2. 如果联动一个文本还觉得慢吗? `onChange` 本不慢,大部分使用值的组件也不慢,没有必要从 `onChange` 源头开始就 `debounce` 。
3. 找到渲染性能最慢的组件(比如 iframe 组件),**对一些频繁导致其渲染的入参进行 `useDebounce` ** 。
下面是一个性能很差的组件,引用了变化频繁的 `text` (这个 `text` 可能是 `onChange` 触发改变的),我们利用 `useDebounce` 将其变更的频率慢下来即可:
``` typescript
const App : React.FC = ( { text } ) = > {
// 无论 text 变化多快,textDebounce 最多 1 秒修改一次
const textDebounce = useDebounce ( text , 1000 )
return useMemo ( ( ) = > {
// 使用 textDebounce,但渲染速度很慢的一堆代码
} , [ textDebounce ] )
} ;
```
使用 `textDebounce` 替代 `text` 可以将渲染频率控制在我们指定的范围内。
### useEffect 注意事项
事实上,`useEffect` 是最为怪异的 Hook,也是最难使用的 Hook。比如下面这段代码:
``` tsx
useEffect ( ( ) = > {
props . onChange ( props . id )
} , [ props . onChange , props . id ] )
```
如果 `id` 变化,则调用 `onChange` 。但如果上层代码并没有对 `onChange` 进行合理的封装,导致每次刷新引用都会变动,则会产生严重后果。我们假设父级代码是这么写的:
``` tsx
class App {
render() {
return < Child id = { this . state . id } onChange = { id = > this . setState ( { id } ) } / >
}
}
```
这样会导致死循环。虽然看上去 `<App>` 只是将更新 id 的时机交给了子元素 `<Child>` ,但由于 `onChange` 函数在每次渲染时都会重新生成,因此引用总是在变化,就会出现一个无限死循环:
新 `onChange` -> `useEffect` 依赖更新 -> `props.onChange` -> 父级重渲染 -> 新 `onChange` ...
想要阻止这个循环的发生,只要改为 `onChange={this.handleChange}` 即可,**`useEffect` 对外部依赖苛刻的要求,只有在整体项目都注意保持正确的引用时才能优雅生效。**
然而被调用处代码怎么写并不受我们控制,这就导致了不规范的父元素可能导致 React Hooks 产生死循环。
因此在使用 `useEffect` 时要注意调试上下文,注意父级传递的参数引用是否正确,如果引用传递不正确,有两种做法:
1. 使用 [useDeepCompareEffect ](https://github.com/streamich/react-use/blob/master/docs/useDeepCompareEffect.md ) 对依赖进行深比较。
2. 使用 `useCurrentValue` 对引用总是变化的 props 进行包装:
``` tsx
function useCurrentValue < T > ( value : T ) : React . RefObject < T > {
const ref = React . useRef ( null ) ;
ref . current = value ;
return ref ;
}
const App : React.FC = ( { onChange } ) = > {
const onChangeCurrent = useCurrentValue ( onChange )
} ;
```
`onChangeCurrent` 的引用保持不变,但每次都会指向最新的 `props.onChange` ,从而可以规避这个问题。
## 总结
如果还有补充,欢迎在文末讨论。
如需了解 Function Component 或 Hooks 基础用法,可以参考往期精读:
- [精读《React Hooks》 ](https://github.com/dt-fe/weekly/blob/v2/079.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md )
- [精读《怎么用 React Hooks 造轮子》 ](https://github.com/dt-fe/weekly/blob/v2/080.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%80%8E%E4%B9%88%E7%94%A8%20React%20Hooks%20%E9%80%A0%E8%BD%AE%E5%AD%90%E3%80%8B.md )
- [精读《useEffect 完全指南》 ](https://github.com/dt-fe/weekly/blob/v2/096.%E7%B2%BE%E8%AF%BB%E3%80%8AuseEffect%20%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%E3%80%8B.md )
- [精读《Function Component 入门》 ](https://github.com/dt-fe/weekly/blob/v2/104.精读《Function%20Component%20入门》.md )
> 讨论地址是:[精读《React Hooks 最佳实践》 · Issue #202 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/202)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。 **
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))