前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React Hook实践总结

React Hook实践总结

原创
作者头像
jadeCarver
修改2021-01-08 20:19:55
1K0
修改2021-01-08 20:19:55
举报
文章被收录于专栏:CS成长之路

最近一年几乎都在使用 TypeScript + Hooks 编写函数式组件,这一篇是我使用 hooks 的一些总结。

开始之前,看一个经典的计数器例子:

代码语言:txt
复制
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        Click
      </button>
    </div>
  );
}
每一帧都是独立的

任意一次渲染中的count常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count值独立于其他渲染。 —— Dan Abramov

在React组件中,通过改变状态来触发组件的重新 render,每次渲染都可以理解为一。在每一帧中,状态只是一个普通的变量,render的时候,它都是独立不变的。

也就是说,在每次渲染中,所有的 state、props 以及 effects 在组件的任意位置都是固定的,我们无法直接获取过去或者未来渲染周期的状态。

state 变化,引发了视图的更新,从直觉上看来,这里是不是使用了数据绑定或者,观察者之类的高级技巧,实际上不是的,它只是函数的重复调用而已,count 是每次调用都独立的局部变量。

更新 state

在react中,state或者props的改变,都会触发重新渲染。函数式组件以参数的形式接受props,props变化,整个组件都会重新渲染。useState在函数式组件内部创建了状态,并提供了一个改变状态的方法。

代码语言:txt
复制
const [count, setCount] = useState(0);

几个值得注意的点:useState的初始值可以是一个简单类型,也可以是复杂类型。同时它还可以接收一个函数,将函数的返回值作为该state的初始值。

代码语言:txt
复制
const a = 1;
const b = 2;
const [count, setCount] = useState(() => a + b);

既然每一帧的渲染中,state 都是独立的,其实就会有一个问题,当我们执行完 setCount 之后,并不会立即拿到最新的 count 的值:

代码语言:txt
复制
const [count, setCount] = useState(0);
setCount(count + 1);
console.log(count); // 0

也就是说,count 的值在本次渲染周期内是固定不变的,直到下一次渲染,count 才会更新为 1.这也是为什么感觉 state 的改变是异步的原因。

获取未来或者过去的state

如果想要获取到最新的state值,则可以通过给setCount方法传入一个函数来执行。

还有一种方法就是使用 useRef,它是一个所有帧共享的变量,你可以在任何时间改变它,然后在它未来的帧中访问它。也就是说,useRef可以为渲染视图的特定一帧打一个快照进行保存。

什么样的数据需要保存为内部 state

在实际使用中,一个组件可能会出现大量的 useState定义,这个时候,我们需要回头反思,如此多的 state 定义是否有必要?

我们知道,react 状态的变化会引发视图的更新,所以将一个变量定义为 state 的标准是:它的改变需要直接引发视图的更新?如果答案是否定的,那就完全不必定义一个 state 出来,而是通过一般的变量将其缓存起来。或者说,使用 useRef是一种不错的选择。

管理复杂状态的两种选择: useReducer + useContext

对于一些需要全局使用的状态,如果需要在多层组件之间传递更新数据,这很容易造成逻辑混乱且不可追踪,则可以通过 useContext 来简化这一过程,以避免显示地在每一层组件之间传递props,子组件可以在任何地方访问到该 context 的值。在下面的例子中,我们将终端的平台和版本通过context注入:

代码语言:txt
复制
const client = {
  mobile: {
    system: 'android',
    version: '8.0.0'
  },
  mac: {
    system: 'MacOS',
    version: '11.0.1'
  }
}
const ClientContext = React.CreateContext({});
const App = () => {
  return (
  		<ClientContext.Provider value={client.mac}>
    		<MyComponent />
    </ClientContext.Provider>
  )
}

const MyComponent = () => {
  const client = useContext(ClientContext);
  return <>当前系统为{client.system},系统版本为{client.version}</>
}

在某一个节点注入 context,该组件及其所有下属组件都会共享这个 context。当该 context 的值发生变化时,其下的所有组件都会重新 render.

useReducer,是改变 state 的另一种方式。顾名思义,就是 reducer 的 hooks 用法。reducer 接受一个改变 state 的方法,以及触发方法的 action,计算之后,返回新的 state.类似于这样 (state, action) => newState.useReducer在某些复杂场景下比 useState更实用,例如一个操作会引发N多个 state 的更新,或者说,state 本身嵌套很多层,更新的逻辑易遗漏,维护起来一片凌乱等等场景。

reducer 是一个纯函数,也就是说,它不包含任何 UI 和副作用操作。也就是说,只要输入的值不变,其输出的值也不会改变。

同样地,reducer 中的数据是 immutable 的,不要直接改变输入的 state,而是应该返回一个新的改变后的 state.

action 是一个用 type 标识的动作,例如对计数器的 increase、decrease等,在 reducer中,可以根据 action type 的不同,采用不同的数据处理。同时,它可以接收第二个参数 payload,传入执行该 action 需要的额外数据。

代码语言:txt
复制
const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer接收一个 reducer 函数,以及一个初始的 state 值,暴露出计算之后的新 state,以及一个 dispatch 方法,它接收一个 action 为参数,用来触发相应的 reducer. 下面使用 useReducer重构计数器的例子:

代码语言:txt
复制
const initialCount = {
  count: 0
};

const countReducer = (state, action) => {
  switch (action.type) {
    case "increase":
      return { count: state.count + 1 };
    case "decrease":
      return { count: state.count - 1 };
    case "reset":
      return { count: action.payload };
    default:
      return initialCount;
  }
};

export default function App() {
  const [state, dispatch] = useReducer(countReducer, initialCount);

  return (
    <div className="App">
      <h1>{state.count}</h1>
      <button onClick={() => dispatch({ type: "increase" })}>+1</button>
      <button onClick={() => dispatch({ type: "decrease" })}>-1</button>
      <button
        onClick={() => dispatch({ type: "reset", payload: initialCount.count })}
      >
        reset
      </button>
    </div>
  );
}

在某些复杂的场景中,reducer 的引入实际上将复杂的 state 更新行为剥离出来,单独在 reducer 之中维护,而组件的核心交互逻辑我们只需要关照 dispatch 了哪个 action,这样使得代码可读性大大提高,组件的核心逻辑也会清晰明了。同时,对于不涉及多层组件交互的状态,并不适合使用 reducer 来维护,这样,反而增加了维护的复杂度。

在一些复杂场景下,结合 useContextuseReducer可以发挥出十分强大的威力。一般的做法是,可以把 state 和 dispatch 方法通过 context 注入,这样,很方便地实现了状态的集中管理和更新。这种方法最好不要滥用,因为集中管理、处处可以变更的方式虽然看起来方便很多,但在 context 的作用范围处处都可以通过 dispatch 来更新 state,这样很容易造成 state 的更新不可追踪。

一般情况下,这种模式适合多层组件状态交互十分密集,且数据具有较好的完整性和共享需要,整个 state 描述的是同一件事,而不是把任意的数据都塞进去维护,这样写起来一时爽,维护起来火葬场~

副作用管理

useEffectuseLayoutEffect都是用来执行视图之外的副作用。前者在每次状态更新且视图也渲染完毕之后执行。后者则是在DOM更新完毕,但页面渲染之前执行,所以会阻塞页面渲染。

如前所述,在每一帧的渲染中,useEffect 中使用的 state 和 props 也是独立不变的。

可以通过给它传入第二个参数,一个依赖数组,来跳过不必要的副作用操作,React 通过对比依赖数组的值是否发生变化来决定是否执行副作用函数。当第二个参数为一个空数组的时候,意味着这个 Effects 只会执行一次。

对于依赖数组,使用不当经常会遇到各种各样的重复渲染的情况。不要添加不必要的依赖在数组中,因为依赖项越多,意味着该 Effects 被多次执行的概览越大。例如,在下面的场景中,如果需要在 Effects 中更新 state,不必将该 state 传入依赖数组,而是通过给 setCount 传入回调的方式去获得当前 state:

代码语言:txt
复制
useEffect(() => {
  // setCount(count + 1); 这种方式会引入一个 state 的依赖项。
  setCount(count => count + 1);
}, [])

在React官方的文档中,还提到了两种需要避免重复渲染的情况及处理方式:

  1. 当依赖项中传入一个函数时,通过使用 useCallback来包裹函数避免函数反复被创建;
  2. 当依赖项中传入数组或者对象等引用类型,通过使用 useMemo来缓存处理它。
使用useMemo和useCallback

如上所述,合理地使用 useMemouseCallback能够避免不必要的渲染。当对象或者数组作为 props 传入的时候,可以使用 useMemo来缓存对象,使其在必要的时候更新:

代码语言:txt
复制
const data = useMemo(() => ({ id}), [id]);

<ComponentA data={data} />

只要 id不变,Component 就不会重新渲染。

useMemo 同样可以用来缓存组件内部的部分 React Element ,以避免重复渲染并没有变化的部分。

使用 useMemo 或者 useCallback 并不是绝对会提升性能。如果你缓存的数据永远不会改变或者说,每一次都会改变,那大可不必使用这两个 hooks,毕竟它们需要额外的计算成本以及存储空间,有的时候得不偿失。

最后,在React哲学一文中,官方给出了一种使用 React 来构建应用的思路,我觉得十分赞。这篇文章中提到,开始的时候,找出应用所需的最小集合,其他数组均有它们计算而出。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 每一帧都是独立的
  • 更新 state
  • 获取未来或者过去的state
  • 什么样的数据需要保存为内部 state
  • 管理复杂状态的两种选择: useReducer + useContext
  • 副作用管理
  • 使用useMemo和useCallback
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档