React Hook

512 阅读6分钟

Hook是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

使用Hook的动机

  1. Hook 使你在无需修改组件结构的情况下复用状态逻辑。在Hook之前,我们可以使用render props和高阶组件来添加可复用的状态逻辑。

    render props就是在react组件之间使用一个值为函数的prop共享代码技术,组件接收一个返回React元素的函数,并在组件内部调用这个函数完成渲染逻辑。(这个prop的名字叫render或其他名字),而且也不一定要放到JSX元素的attribute列表中,也可以放在元素标签内部。

    高阶组件(Higher Order Component)是参数为组件,返回值为新组件的函数。HOC将组件包装在容器组件中来组成新的组件,来完成一些可复用的逻辑。

    但这两个方案需要重新组织组件的结构,可能使代码难以理解。使用Hook可以从组件中提取状态逻辑,使这些逻辑可以单独测试且复用,并无需修改组件结构。

  2. 我们维护组件时,随着时间推移,组建的生命周期函数往往变得臃肿,会有很多不相关的状态逻辑。Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。

  3. Hook 使你在非 class 的情况下可以使用更多的 React 特性。无需理解class

Hook的特性

Hook是钩子的意思,就是我们使用函数式组件时,尽量写成纯函数,如果需要React state和生命周期等特性的函数,就使用Hook完成这些原本在class中才能实现的需求。Hook是

  • 完全可选的
  • 100%向后兼容
  • 现在可用
  • 没有计划从React中移除class

Hook使用规则

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

React怎么知道哪个state对应哪个useState,答案是靠Hook执行的顺序,如果我们在条件语句中使用hook,那么很有可能导致前后两次hooks执行顺序发生改变,导致bug产生。如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部。

  • 只能在React的函数式组件中调用,不要在其他JavaScript函数中调用。

React中几种常见的Hook

useState

相当于类组件中的state。

当我们调用useState时,我们传入一个参数,作为这个state的初始值,它返回一个有两个元素的数组,分别代表当前state的值以及更新这个state的函数,比如[count, setCount] = useState(0),通过数组解构,我们得到两个变量,count是这个state的变量名,我们可以调用setCount更新count这个state。

使用useState时,应该使用单个state变量还是使用一个对象打包所有state?

React官方推荐把 state 切分成多个 state 变量,因为每次更新一个state,会用新值整个替换旧值,不像class组件的setState那样合并。因此如果非要用一个对象包含所有state,state的更新要这样实现:

 const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
 ...
 / 展开 「...state」 以确保我们没有 「丢失」 widthheight
      setState(state => ({ ...state, left: e.pageX, top: e.pageY }));

useEffect

useEffect(() => {...}, []);

数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。我们可以使用useEffect完成在函数式组件中这些功能。

清除操作:只需在返回值中返回一个函数,那么React会在组件卸载的时候执行清除操作并调用它,比如取消订阅。

默认情况下,useEffect会在每次渲染后执行,如果要通知React跳过对effect的调用,即不要每次渲染都调用,我们可以传递数组作为第二个参数,比如我们传入[count]作为第二个参数,那么只有当count发生改变时,这个副作用函数才会被执行。

//相当于componentDidMount和componentWillUnmount
useEffect(() => {...}, [])
//会紧紧盯着count,只要count值改变,就会执行
useEffect(() => {...}, [count])
//默认情况,相当于componentDidMount和componentDidUpdate
useEffect(() => {...})

useContext

在组件之间共享状态的钩子。在React中,如果我们要传递变量,可以使用props属性向下传递给子组件。这种方法很简单,但当我们想把变量传递给子组件的子组件时,就需要使用子组件的props再往下传递,这样就形成了props的深度注入。随着props注入越深,组件更新的频率也越来越高,UI效率也就越来越慢。

而useContext就是来解决非父子组件的数据共享问题的。

假设说我们需要全局共享一个变量,username。

第一步,我们要利用React Context API,在组件外部创建一个context,并传入默认初始值。

const defaultContextValue = { username: 'sxx' };
const appContext = React.createContext(defaultContextValue)

然后,为了使App组件和其子组件能共享这个username,我们要用appContext.Provider把整个render函数包裹起来。并且要把defaultContextValue注入到value属性中。

<appContext.Provider value={defaultContextValue}>
  <App />
</appContext.Provider>

接下来,我们就可以在他的子孙组件中访问到username这个变量了。

有两种方法:

  1. 利用appContext.Consumer组件,在组件内部使用花括号,使用箭头函数在其内部共享数据:
import { appContext } from ...
...
  <appContext.Consumer>
  {(value) => {
    {/*在这里可以访问到全局的username啦*/}
    <h1>{value.username}</h1>
  }}
</appContext.Consumer>
  1. 使用React Hook。利用useContext钩子函数,我们不用改变代码的结构,能很方便的在组件中获取数据:
import { useContext } from 'react';
const value = useContext(appContext);
//接着就可以在return中直接使用value了

useContext这个hook极大的减少了模板代码,降低了代码层级,也消灭了多个consumer嵌套的可能性。

useReducer

const [state, dispatch] = useReducer(reducer, initialState);

这个钩子接收一个reducer和initialState为参数,返回当前状态和dispatch action的函数,可以在不使用redux的情况下,管理数据状态。

const myReducer = (state, action) => {
  switch(action.type)  {
    case('countUp'):
      return  {
        ...state,
        count: state.count + 1
      }
    default:
      return  state;
  }
}

//组件代码
function App() {
  const [state, dispatch] = useReducer(myReducer, { count:   0 });
  return  (
    <div className="App">
      <button onClick={() => dispatch({ type: 'countUp' })}>
        +1
      </button>
      <p>Count: {state.count}</p>
    </div>
  );
}

自定义Hook

自定义 Hook 的命名以use开头,不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。

参考资料:

  1. React Hook官方文档
  2. 阮一峰的网络日志——React Hooks 入门教程