react hooks 学习纪要

278 阅读7分钟

v16.8 版本之前,组件的标准写法是类(class component)。v16.8 版本引入了全新的 API(hooks component),叫做 React Hooks,颠覆了以前的用法。

函数组件(function component)

  • React 早期就支持函数组件,但是有重大限制,必须是纯函数,不能包含状态,也不支持生命周期方法,因此无法取代类。
  • React Hooks 的设计目的,就是加强版函数组件,完全不使用"类",就能写出一个全功能的组件。
// 早期函数式组件
function Welcome(props) {
    return <h1>Hello, {props.name}</h1>;
}

React Hooks

useState 状态钩子

  • 通过在函数组件里调用它来给组件添加一些内部 state。React 会在重复渲染时保留这个 stateuseState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。
  • 可在组件中多次使用
// hooks component
import React, { useState } from 'react';

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量
  // 这里的 setCount 就类似 this.setState({count: any})
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect 副作用钩子

  • 你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。
  • useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。
  • 副作用函数还可以通过返回一个函数来指定如何“清除”副作用。
  • 可在组件中多次使用
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);
  
  // useEffect()接受两个参数。
  // 第一个参数是一个函数,异步操作的代码放在里面。
  // 第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。
  // 第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()
  // 相当于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用浏览器的 API 更新页面标题
    // 执行副作用
    document.title = `You clicked ${count} times`;

    // 消除副作用
    return () => {
      document.title = "";
    } 
  }, [dependencies]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useContext 共享状态钩子

  • 如果需要在组件之间共享状态,可以使用useContext()
import React, {createContext} from 'react';
// 建立 context
const AppContext = createContext({});
<AppContext.Provider value={{
    username: 'superawesome'
}}>
    <div className="App">
        <Navbar/>
        <Messages/>
    </div>
</AppContext.Provider>
  • Navbar 中使用共享状态
const Navbar = () => {
    const { username } = useContext(AppContext);
    return (
    <div className="navbar">
      <p>AwesomeSite</p>
      <p>{username}</p>
    </div>
    );
}
  • Messages 中使用共享状态
const Messages = () => {
    const { username } = useContext(AppContext);
    
    return (
    <div className="messages">
      <h1>Messages</h1>
      <p>1 message for {username}</p>
      <p className="message">useContext is awesome!</p>
    </div>
    )
}

useReducer action 钩子

  • React 本身不提供状态管理功能,通常需要使用外部库。这方面最常用的库是 Redux
  • Redux 的核心概念是,组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是(state, action) => newState
  • useReducers()钩子用来引入 Reducer 功能。
import React, {useReducer} from 'react';

function App(){
    // 接受 Reducer 函数和状态的初始值作为参数,返回一个数组。
    // 数组的第一个成员是状态的当前值,
    // 第二个成员是发送 action 的 dispatch 函数。
    const [state, dispatch] = useReducer((state, action) => {
        switch(action.type)  {
         case('countUp'):
           return  {
             ...state,
             count: state.count + 1
           };
         default:
           return state;
        }
    }, {count: 0});

    return (
       <div className="App">
         <button onClick={() => dispatch({ type: 'countUp' })}>
           +1
         </button>
         <p>Count: {state.count}</p>
       </div>
    );
}
  • 由于 Hooks 可以提供共享状态和 Reducer 函数,所以它在这些方面可以取代 Redux。但是,它没法提供中间件(middleware)和时间旅行(time travel),如果你需要这两个功能,还是要用 Redux
  • 惰性初始化,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg);这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 stateaction 做处理提供了便利
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>
    </>);
}
  • 如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

useCallback 回调钩子

  • 把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
// useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
// 依赖项数组不会作为参数传给回调函数。
// 虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。
const memoizedCallback = useCallback(() => {
    doSomething(a, b);
}, [a, b]);

useMemo 记忆钩子

  • 传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo
  • 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
// 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useRef

  • 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>
    </>
    );
}
  • 应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
  • ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

useImperativeHandle

  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
// useImperativeHandle(ref, createHandle, [deps])
function FancyInput(props, ref) {
    const inputRef = useRef();
    useImperativeHandle(ref, () => ({
        focus: () => {
          inputRef.current.focus();
        }
    }));
    return <input ref={inputRef} {...props} />;
}
FancyInput = forwardRef(FancyInput);
// 使用 FancyInput
// 父组件可以调用 inputRef.current.focus()。
<FancyInput ref={inputRef} /> 

useLayoutEffect

  • 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
  • 如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffectcomponentDidMountcomponentDidUpdate 的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect