React Hooks 完整学习笔记

设计动机

  1. 组件之间复用状态逻辑很难

  2. 复杂组件变得难以理解

  3. 难以理解的 Class

详细参考这里

使用姿势

useState

import React from 'react';
const { useState } = React;

// https://zh-hans.reactjs.org/docs/hooks-state.html

// useState 是最简单的一个 hook
// 唯一需要注意的是不要尝试在循环或条件等不稳定的代码结构中编写
// 原因在这里 -> https://github.com/brickspert/blog/issues/26

export default function UseState() {
  const [num1, setNum1] = useState(0);
  const [num2, setNum2] = useState(0);
  console.warn('render');
  return (
    <div>
      useState Demo
      <h4>num1:{num1}</h4>
      <h4>num2:{num2}</h4>
      <button onClick={() => setNum1(num1 + 1)}>add num1</button>
      <button onClick={() => setNum2(n => n + 1)}>add num2</button>
    </div>
  );
}

useEffect

import React from 'react';
const { useState, useEffect } = React;

// https://zh-hans.reactjs.org/docs/hooks-effect.html
// https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/

// 适用场景:
// 1. 模拟钩子函数可以进行清理操作

export default function UseEffect() {
  const [num1, setNum1] = useState(0);
  const [num2, setNum2] = useState(0);

  // 相当于 componentDidMount + componentDidUpdate + componentWillUnmount
  useEffect(() => {
    console.log('useEffect1');
    return () => {
      console.log('_useEffect1');
    };
  });

  // 相当于 componentDidMount + componentWillUnmount
  // 注意 deps 参数为空数组,不同于不传!!
  useEffect(() => {
    console.log('useEffect2');
    return () => {
      console.log('_useEffect2');
    }
  }, []);


  // 相当于 componentDidMount + componentDidUpdate + componentWillUnmount
  // 相比于第一种情况更加精确
  useEffect(() => {
    console.log('useEffect3');
    return () => {
      console.log('_useEffect3');
    }
  }, [num1]);

  console.warn('render');
  return (
    <div>
      useEffect Demo
      <h4>{num1}</h4>
      <button onClick={() => setNum1(num1 + 1)}>更新 num1</button>
      <br />
      <br />
      <h4>{num2}</h4>
      <button onClick={() => setNum2(num2 + 1)}>更新 num2</button>
    </div>
  );
}

useContext

import React, { useContext } from 'react';

const { useState, createContext } = React;

// https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext

// 适用场景:
// 1. 状态共享

// 唯一需要注意的是:
// 当组件上层最近的 <SizeContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 SizeContext provider 的 context value 值。
// 即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

const SizeContext = createContext({
  size: 0,
  // 给子组件更改 context value 值暴露接口
  setSize(size: number) { console.log(size) },
});


function ChildCom() {
  const { size, setSize } = useContext(SizeContext);

  function updatSize() {
    setSize(size + 1);
  }
  console.warn('child srender');
  return (
    <div>
      <h4>子组件 size:{size}</h4>
      <button onClick={updatSize}>累加</button>
    </div>
  )
}

export default function UseContext() {
  const [size, setSize] = useState(0);
  console.warn('father render');
  const value = {
    size,
    setSize
  }
  return (
    <div>
      <SizeContext.Provider value={value}>
        <h4>根组件 size:{size}</h4>
        <ChildCom />
      </SizeContext.Provider>
    </div >
  );
}

useMemo

import React from 'react';
const { useState, useMemo } = React;

// https://zh-hans.reactjs.org/docs/hooks-reference.html#usememo

// 适用场景:
// 1. 性能优化:减少不必要的重复计算

function getGreetText(name: string) {
  console.log('重新计算');
  return 'hello' + name;
}

// function ChildNormal(props: { name1: string, name2: string }) {
//   // 不管是 name1 还是 name2 变化都会导致重新计算
//   const greet = getGreetText(props.name1);
//   return (
//     <>
//       {greet}
//     </>
//   );
// }

function ChildUseMemo(props: { name1: string, name2: string }) {
  // 只有在 name1 变化时候才会计算
  const greet = useMemo(() => getGreetText(props.name1), [props.name1]);
  console.log('child render');
  return (
    <>
      <br />
      name1: {props.name1}
      <br />
      name2: {props.name2}
      <br />
      greet: {greet}
    </>
  );
}

export default function UseMeno() {
  const [name1, setName1] = useState('');
  const [name2, setName2] = useState('');
  return (
    <div>
      name1:<input type="text" onChange={(e) => setName1(e.target.value)} />
      <br />
      <br />
      name2:<input type="text" onChange={(e) => setName2(e.target.value)} />
      <br />
      <br />
      {/* greet normal:<ChildNormal name1={name1} name2={name2} /> */}
      <br />
      <br />
      greet useMemo:<ChildUseMemo name1={name1} name2={name2} />

    </div>
  );
}

useCallback

import React, { useCallback } from 'react';

const { useState, memo } = React;

// https://zh-hans.reactjs.org/docs/hooks-reference.html#usecallback

// useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

// 适用场景:
// 1. 性能优化:将句柄传入做了 memo 的子组件

interface IChildProps {
  count: number;
  onAdd(): void;
}

const Child1 = memo(function (props: IChildProps) {
  console.log('Child1 Render');
  return (
    <div style={{ border: '1px solid #000' }}>
      <h4>Child1</h4>
      count:{props.count}
      <button onClick={props.onAdd}>add</button>
    </div>
  );
});

const Child2 = memo(function (props: IChildProps) {
  console.log('Child2 Render');
  return (
    <div style={{ border: '1px solid #000' }}>
      <h4>Child2</h4>
      count:{props.count}
      <button onClick={props.onAdd}>add</button>
    </div>
  );
});

export default function UseCallback() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const onAddCount1 = () => {
    setCount1(count1 + 1);
  }

  // 标记是稳定的,count1 变化不会影响 Child2 的渲染
  const onAddCount2 = useCallback(() => {
    setCount2(count2 + 1);
  }, [count2]);

  console.log('Wrap Renders');
  return (
    <div>
      <Child1 count={count1} onAdd={onAddCount1} />
      <br />
      <Child2 count={count2} onAdd={onAddCount2} />
    </div>
  );
}

useLayoutEffect

import React from 'react';

const { useState, useLayoutEffect, useEffect } = React;

// https://zh-hans.reactjs.org/docs/hooks-reference.html#uselayouteffect

// 适用场景:
// 1. 解决闪烁问题

function Child1() {
  const [value, setValue] = useState(0);

  // 异步更新会出现闪烁
  useEffect(() => {
    if (value === 0) {
      setValue(10 + Math.random() * 200);
    }
  }, [value]);

  console.log('render', value);

  return (
    <div>
      {value === 0 ? <h1>xiixix</h1> : <h4>value: {value}</h4>}
      <button onClick={() => setValue(0)}>click me</button>
    </div>
  );
}

function Child2() {
  const [value, setValue] = useState(0);

  // 同步更新将不会出现闪烁
  useLayoutEffect(() => {
    if (value === 0) {
      setValue(10 + Math.random() * 200);
    }
  }, [value]);

  console.log('render', value);

  return (
    <div>
      {value === 0 ? <h1>xiixix</h1> : <h4>value: {value}</h4>}
      <button onClick={() => setValue(0)}>click me</button>
    </div>
  );
}
export default function UseLayoutEffect() {
  return (
    <div>
      <Child1 />
      <hr />
      <hr />
      <Child2 />
    </div>
  );
}

useRef

import React, { useEffect, ChangeEvent, useState } from 'react';
const { useRef } = React;

// https://zh-hans.reactjs.org/docs/hooks-reference.html#useref
export default function UseRef() {
  const [num, addNum] = useState(0);
  // 1. 用于获取 DOM 
  const ref1 = useRef<HTMLInputElement>(null);
  // 2. 用作实例属性的存储,useRef 在整个组件生命周期都会保持不变
  const ref2 = useRef<string>('0');

  useEffect(() => {
    console.log('num', ref2.current);
    console.log('ref2.current', ref2.current);
  });

  const onClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    ref1.current?.focus();
  };

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    ref2.current = e.target.value
  }

  console.warn('render');
  return (
    <div>
      useRef Demo
      <input ref={ref1} type="text" onChange={onChange} />
      <br />
      <button onClick={onClick}>Focus</button>
      <br />
      <button onClick={() => addNum(num + 1)}>addNum</button>
    </div>
  );
}

useImperativeHandle

import React from 'react';
const { useRef, useImperativeHandle, forwardRef } = React;

// https://zh-hans.reactjs.org/docs/hooks-reference.html#useimperativehandle
// 适用场景: ref 转发时候代理一层,做 API 的上层封装

interface IChildRef {
  getHeight(): number;
}

const Child = forwardRef((props: {}, ref: React.Ref<IChildRef>) => {
  const divRef = useRef<HTMLDivElement>(null);

  useImperativeHandle(ref, () => ({
    getHeight: () => {
      console.log('计算了高度');
      return divRef.current?.clientHeight || 0;
    }
  }));

  return (
    <div ref={divRef} style={{ height: '100px', width: '100px', border: '1px solid #000' }}>
      i am child
    </div>
  );
});

export default function UseImperativeHandle() {
  const childRef = useRef<IChildRef>(null);

  function getChildHeight() {
    console.log(childRef.current?.getHeight());
  }

  return (
    <div>
      useImperativeHandle Demo
      <Child ref={childRef} />
      <button onClick={getChildHeight}>click me</button>
    </div>
  );
}

useReducer

import React from 'react';

const { useReducer } = React;

// https://zh-hans.reactjs.org/docs/hooks-reference.html#usereducer

// 适用场景;
// 1. 复杂的数据类型,需要差量更新
// 2. 可以获取到上一次的数据
// 3. 性能优化:稳定的 dispatch 句柄

function reducer(state: { count: number }, action: { type: 'increment' | 'decrement' }) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

export default function UseReducer() {
  console.warn('render1');
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      Count: {state.count}
      <br />
      <br />
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
}

useContext + useReducer 实现全局共享状态

import React from 'react';

const { useReducer, createContext, useContext } = React;

type ISizeTypes = 'addWidth' | 'addHeight';
type ISize = { width: number, height: number }

const SizeContext = createContext([
  {
    width: 0,
    height: 0
  },
  a => { }
]) as React.Context<[ISize, (a: { type: ISizeTypes }) => void]>;

const useSize = () => {
  const [size, dispatch] = useContext(SizeContext);
  function addWidth() {
    dispatch({ type: 'addWidth' });
  }
  function addHeight() {
    dispatch({ type: 'addHeight' });
  }
  return { size, addWidth, addHeight };
};


function reducer(state: ISize, action: { type: ISizeTypes }) {
  switch (action.type) {
    case 'addWidth':
      return { ...state, width: state.width + 1 };
    case 'addHeight':
      return { ...state, height: state.height + 1 };
    default:
      throw new Error();
  }
}

function Com1() {
  const { size, addHeight, addWidth } = useSize();
  return (
    <div>
      组件1
      <br />
      宽度: {size.width}
      <br />
      高度: {size.height}
      <br />
      <button onClick={addHeight}>增加高度</button>
      <button onClick={addWidth}>增加宽度</button>
    </div>
  );
}

function Com2() {
  const { size, addHeight, addWidth } = useSize();
  return (
    <div>
      组件2
      <br />
      宽度: {size.width}
      <br />
      高度: {size.height}
      <br />
      <button onClick={addHeight}>增加高度</button>
      <button onClick={addWidth}>增加宽度</button>
    </div>
  );
}

export default function UseReducer() {
  console.warn('render1');
  const sizeContext = useReducer(reducer, { width: 0, height: 0 });
  return (
    <>
      <SizeContext.Provider value={sizeContext}>
        <Com1 />
        <hr />
        <Com2 />
      </SizeContext.Provider>
    </>
  );
}

两个原则

  1. 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook(这是为什么?)

  2. 只在 React 函数中调用 Hook

自定义 Hooks

这部分建议直接阅读市面上一些优秀 hooks 库的源码:

react-use

awesome-react-hooks

@umi/hooks


参考资料: