React hooks可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性,它的出现提供了一种新型可复用组件间状态逻辑的途径,替换以往采用的HOC和Render Props的形式.

新增该特性的原由: https://zh-hans.reactjs.org/docs/hooks-intro.html#motivation

🐶 特性

  1. 多个状态不会产生嵌套,写法还是平铺的(renderProps 可以通过 compose 解决,可不但使用略为繁琐,而且因为强制封装一个新对象而增加了实体数量)。
  2. Hooks 可以引用其他 Hooks。
  3. 更容易将组件的 UI 与状态分离。

    精读《React Hooks》

React hooks带来了更强的组合设计模式, 与状态的隔离更加清晰.

🐷生命周期

React hooks的生命周期
Snipaste_2019-12-25_18-05-50.png

React class component的生命周期
Snipaste_2019-12-25_18-11-31.png

相比class component, 仅缺少对于不常见的 getSnapshotBeforeUpdate 和 componentDidCatch, 其他的阶段都有对应等价关系.

👍 React hooks

1. 使用useState

  1. const [state, setState] = useState(initialState);

1.1 initialState

仅初次渲染起作用,往后的渲染被忽略

若initialState是一个函数, 则在初始渲染时调用并返回结果

1.2 state

此时state等价于initialState

1.3 setState

提供给更新state使用的函数。

因为setState是不可变的, 故可在使用useEffect和useCallback依赖列表中省略setState 其与class中的setState方法不同, 不能自动合并更新对象

setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

2. 使用useEffect

useEffect相似等价于componentDidMount, componentDidUpdate , componentWillUnmount, 以下展示三个场景

2.1 每次渲染

React.useEffect( () => {
   Console.log("useEffect runs");
});

2.2 仅当依赖改变时渲染

React.useEffect( () => {
   Console.log("useEffect runs");
}, [state]);

2.3 组件销毁时

useEffect(() => {
  console.log('mounted');
  const subscription = props.source.subscribe();
  return () => {
      console.log('unmounting...');
    subscription.unsubscribe();
  }
}, []) // [] 空数组代表此effect仅在 mounted 和 unmounting 调用一次

建议使用 eslint-plugin-react-hooks 和 exhaustive-deps 配合使用

但useEffect又与componentDidMount、componentDidUpdate不同, 如下图所示, useEffect是在browser paints screen更新后进行延迟处理操作的。
1_-QHrX7G1g78HBTGkzwOPLQ.png

但不是所有的effect都需要延迟, 像处理dom等操作时, 应该在browser paints screen前就要处理完成, 这时就需要使用 useLayoutEffects, 否则就会产生视觉的突兀感, 反正记着优先使用useEffect, 若处理中遇到异常则使用 useLayoutEffect, 见下面.

3. 使用useContext

useContext(MyContext) 等价于 class 中的 static contextType = MyContext

import React, { useContext } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const Context = React.createContext();

function Display() {
  const value = useContext(Context);
  return <div>{value}, I am your Father.</div>;
}

function App() {
  return (
    <Context.Provider value={"Luke"}>
      <Display />
    </Context.Provider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

4. 使用useReducer

useState的加强版, 可代替去处理复杂深层结构的state, 本质就是使用 dispatch 替换 callback
**

局部状态不推荐使用 useReducer , 会导致函数内部更复杂, 难以阅读, 在多组件通信时可结合 useContext一起使用

const initialState = {count: 0};

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

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

4.1 initialState

此时state等价于initialState

惰性初始化

function init(initialState) {
  return {count: initialState};
}

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

5. 使用useCallback

useCallback(fn, deps) 等价于 useMemo(() => fn, deps), 初始化后会fn会仅当deps发生变化时重新渲染, 主要用作缓存函数

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

6. 使用useMemo

useMemo会在渲染时运行callback并赋值, 主要用作缓存复杂的函数的结果值

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

7. 使用useRef

useRef是一种主要用来访问Dom的方式, 无论节点如何变化, 其属性current仍指向dom节点

const refContainer = useRef(initialValue);

7.1 initialValue

此时initialValue 等价于refContainer.current

7.2 refContainer

refContainer是一个mutable的ref object, 其在整个component生命周期内保持不变

8. 使用useImperativeHandle

useImperativeHandle 主要用来暴露本身component的自定义属性给父级component使用

该例子展示可以在父级组件中调用子组件暴露的方法和对象:

import React, {
  useRef,
  useEffect,
  useImperativeHandle,
  forwardRef
} from "react";
import { render } from "react-dom";

const ChildInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    ref: inputRef.current,
    doSomething: console.log
  }));

  return <input type="text" name="child input" ref={inputRef} />;
});

function App() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.ref.focus();
    inputRef.current.doSomething("打印");
  }, []);

  return (
    <div>
      <ChildInput ref={inputRef} />
    </div>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);

codesandbox.png

9. 使用useLayoutEffect

useLayoutEffect是一种用于在浏览器绘制前同步渲染页面的方案

该例子如果使用useEffect代替useLayoutEffect则会出现节点闪烁的情况:

import React, { useState, useLayoutEffect, useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const LayoutEffectComponent = () => {
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);
  const el = useRef();

  useLayoutEffect(() => {
    console.log("el.current", el.current.clientWidth);
    setWidth(el.current.clientWidth);
    setHeight(el.current.clientHeight);
  });

  return (
    <div>
      <h1>useLayoutEffect Example</h1>
      <h2>textarea width: {width}px</h2>
      <h2>textarea height: {height}px</h2>
      <textarea
        onClick={() => {
          setWidth(0);
        }}
        ref={el}
      />
    </div>
  );
};

function App() {
  return (
    <div className="App">
      <LayoutEffectComponent />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

10. 使用useDebugValue

useDebugValue 用来在react devTools中显示自定义的hook标签

useDebugValue(value)

🦄 实践例子

下面展示一个用react hooks构建todo列表的demo:

image.png

codesandbox.png

1. 组件

image.png
App.js

组件也可以写成自定义模式
image.png
useInputForm.js

2. 逻辑

抽离相关逻辑为了更好的维护和复用, 状态与 UI 的界限会越来越清晰
image.png
useTodo.js

另相关代码已同步与Gist, 使用CodeExpander即可生成以上代码图片.

🐲FAQ

1. 可以在函数内直接声明常量或普通函数吗?

不可以, 考虑每次渲染会重新执行, 推荐放到函数组件外层避免性能问题或使用 useCallback 声明.

👻常用库

react-use

(持续更新…)