1

这篇为react hooks的学习笔记,仅对需要关注的重点做了记录。有关react hooks详细说明,参见官网https://reactjs.org/docs/hook...

Rules of Hooks

  • 不能将 hooks 放在循环、条件语句或者嵌套方法内。react是根据hooks出现顺序来记录对应状态的
  • 只在 function 组件和自定义 hooks 中使用 hooks。
  • 命名规范

    • useState 返回数组的第二项以 set 开头(仅作为约定)
    • 自定义 hooks 以 use 开头(可被 lint 校验)

API

  • useState
    useState 可传任意类型的变量或者返回任意类型变量的 function。
    useState 返回数组的第二个参数(setter),可传任意类型的变量,或者一个接收 state 旧值的 function,其返回值作为 state 新值

    function Counter({ initialCount }) {
      const [count, setCount] = useState(initialCount);
      // Lazy initialization
      const [state, setState] = useState(() => {
        const initialState = someExpensiveComputation(props);
        return initialState;
      });
      return (
        <>
          Count: {count}
          <button onClick={() => setCount(0)}>Reset</button>
          <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
          <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
        </>
      );
    }

    set 方法不会像 setState 一样做 merge,所以建议如果数据结构简单,可以将变量根据数据结构需要放在不同的 useState 中,避免大量使用类似{...state, value}形势。如果数据结构复杂,建议使用 useReducer 管理组件的 state

  • useEffect

    useEffect(effect, array);

    effect 函数将在 componentDidAmount 时触发和 componentDidUpdate 时有条件触发。可以返回一个函数(returnFunction),returnFunction 将会在 componentWillUnmount 时触发和在 componentDidUpdate 时先于 effect 有条件触发。
    与 componentDidAmount 和 componentDidUpdate 不同之处是,effect 函数触发时间为在浏览器完成渲染之后。 如果需要在渲染之前触发,需要使用 useLayoutEffect。
    第二个参数 array 作为有条件触发情况时的条件限制。

    • 如果不传,则每次 componentDidUpdate 时都会先触发 returnFunction(如果存在),再触发 effect。
    • 如果为空数组[],componentDidUpdate 时不会触发 returnFunction 和 effect。
    • 如果只需要在指定变量变更时触发 returnFunction 和 effect,将该变量放入数组。
  • useContext
    和consumer类似,仍然需要与Provider配合使用

    const Context = React.createContext('light');
    
    // Provider
    class Provider extends Component {
      render() {
        return (
          <Context.Provider value={'dark'}>
            <DeepTree />
          </Context.Provider>
        )
      }
    }
    // Consumer
    function Consumer(props) {
      const context = useContext(Context);
      return (
        <div>
          {context} // dark
        </div>
      );
    }
  • useReducer
    用于管理复杂结构的状态对象,与redux的核心逻辑一致。

    const [state, dispatch] = useReducer(reducer, initialState, {
      type: 'reset',
      payload: initialCount
    });
    // demo
    const TodosDispatch = React.createContext(null);
    
    function TodosApp() {
      // Tip: `dispatch` won't change between re-renders
      const [todos, dispatch] = useReducer(todosReducer, initialState);
    
      return (
        <TodosDispatch.Provider value={dispatch}>
          <DeepTree todos={todos} />
        </TodosDispatch.Provider>
      );
    }
  • useCallback
    useCallback和下面的useMemo是非常实用的提升性能的小工具。

    const memoizedCallback = useCallback(
      () => {
        doSomething(a, b);
      },
      [a, b]
    );

    useCallback返回一个memoized函数,在参数a和b都没有改变时,总是返回同一个函数。
    其具体实现逻辑基本如下:

    let memoizedState = null;
    function useCallback(callback, inputs) {
      const nextInputs =
        inputs !== undefined && inputs !== null ? inputs : [callback];
      const prevState = memoizedState;
      if (prevState !== null) {
        const prevInputs = prevState[1];
        if (areHookInputsEqual(nextInputs, prevInputs)) {
          return prevState[0];
        }
      }
      memoizedState = [callback, nextInputs];
      return callback;
    }

    注:第二个参数目前只用于指定需要判断是否变化的参数,并不会作为形参传入回调函数。建议回调函数中使用到的变量都应该在数组中列出。以后的版本可能会将第二项数组参数移除,自动判断回调函数中使用到的变量是否变化来判断返回结果。

  • useMemo

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

    与useCallback类似,返回一个memoized函数执行结果。
    useMemo(() => fn, inputs) 等价于 useCallback(fn, inputs)
    useMemo可用于实现PureComponent子组件:

    function Parent({ a, b }) {
      // Only re-rendered if `a` changes:
      const child1 = useMemo(() => <Child1 a={a} />, [a]);
      // Only re-rendered if `b` changes:
      const child2 = useMemo(() => <Child2 b={b} />, [b]);
      return (
        <>
          {child1}
          {child2}
        </>
      )
    }
  • useRef

    const refContainer = useRef(initialValue);

    类似 react.createRef()。
    在使用hooks的function component中,useRef不仅可以用来做DOM的引用,还可以做来作为类似class的实例属性,因为相同位置的useRef()每次返回的都是同一个对象。

    function Timer() {
      const intervalRef = useRef();
    
      useEffect(() => {
        const id = setInterval(() => {
          // ...
        });
        intervalRef.current = id;
        return () => {
          clearInterval(intervalRef.current);
        };
      });
    
      // ...
    }

    利用useRef()的这种特性,还可以做很多其他有趣的事情,例如获取previous props或previous state:

    function Counter() {
      const [count, setCount] = useState(0);
    
      const prevCountRef = useRef();
      useEffect(() => {
        prevCountRef.current = count;
      });
      const prevCount = prevCountRef.current;
    
      return <h1>Now: {count}, before: {prevCount}</h1>;
    }
  • useImperativeMethods

    function FancyInput(props, ref) {
      const inputRef = useRef();
      useImperativeMethods(ref, () => ({
        focus: () => {
          inputRef.current.focus();
        }
      }));
      return <input ref={inputRef} ... />;
    }
    FancyInput =  React.forwardRef(FancyInput);

    useImperativeMethods提供了父组件直接调用子组件实例方法的能力。
    上例中,一个包含 <FancyInput ref={fancyInputRef} /> 的父组件,就可以调用 fancyInputRef.current.focus().

Tips

  • 目前暂时没有与getSnapshotBeforeUpdate和componentDidCatch相匹配的hooks。
  • 使用ESlint plugin(eslint-plugin-react-hooks)做代码校验。
  • Hooks的调用只能放在function component(首字母大写的function)或者自定义Hook(名为useXxxx的function)中。
  • Hooks在组件的每次render时都以完全相同的顺序执行。
  • 用React.memo来实现shouldComponentUpdate。

    const Button = React.memo((props) => {
      // your component
    });
  • 请将refs理解成class中的实例属性。

梦梦她爹
1.8k 声望122 粉丝