前言

如果你是在使用React >= 16.8的版本,那么你可以使用hooks在编写你的组件,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

为什么要使用 Hooks

  1. 提供代码逻辑复用的另一种选择(自定义 Hook),其他选择Render PropsHOCmixins(不推荐使用)
  2. 没有了 class 复杂的生命周期,组件写起来更清爽。性能也更好。
  3. 官方推荐使用,后续官网应该会不断完善Hooks

Hooks 使用指南

useState

通常我们在使用 class 组件时,会在constructor中初始化stateconstructor只会在组件实例化时执行一次。同样的,使用useState可以实现上述的操作。

如下代码initialCount,只会在组件初始化时赋值给count,后续如果要修改count,需要通过setCount来进行修改,这和this.setState的使用方式也是一样的

const [count, setCount] = useState(initialCount);

如果你在初始化useState,需要进行计算,可以使用如下方式

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

注意

  1. 与 class 组件中的 setState 方法不同的是useState 不会自动合并更新对象。所以需要将一个完整的对象传递给useState
// class
this.state = {
  name: "smw",
  age: 18
};
this.setState({
  age: 20
});

// hooks
const [state, setState] = useState({
  name: "smw",
  age: 18
});
setState(preState => {
  return {
    ...preState,
    age: 20
  };
});
  1. 在调用setState时,会将新老设置的值进行 Object.is 比较算法 。所以需要使用展开运算符或者Object.assign来声明新值。
// 错误写法
const [state, setState] = useState({
  name: "smw",
  age: 18
});

setState(preState => {
  preState.age = 20;
  return preState;
});

// 正确写法
setState(preState => {
  return {
    ...preState,
    age: 20
  };
});

ps:但是上述的操作其实并不够优雅,并且当你可能只是需要修改数组中的某个值,甚至你操作的数据是Set或者Map类型的,那处理起来就更复杂了。所以可能你需要immerjs来帮助你。

useReducer

useState的升级方案,如果你的useState存在比较复杂的处理场景可以使用useReducer来处理,其思想和使用方式类似Redux中的reducer

以下是一个官方demo,实现了一个计数器

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>
    </>
  );
}

ps:

  1. useReducer可以获取上一次的state
  2. 可以直接向子组件传递dispatch,实现父子组件之前的通信。

useRef、useImperativeHandle

  • useRef:在Function组件中,获取子组件ref
  • useImperativeHandle:在Function组件中,定义可被父组件获取到的ref

useRef

无论是获取Function组件、class组件还是真实dom,方法都是一样的

const Index = () => {
  const fnChildEl = useRef(null);
  const inputEl = useRef(null);
  const classChildEl = useRef(null);

  useEffect(() => {
    console.log("fnChildEl", fnChildEl);
    console.log("inputEl", inputEl);
    console.log("classChildEl", classChildEl);
  }, []);

  return (
    <div>
      <input ref={inputEl} type="text" />
      <FnChild ref={fnChildEl} />
      <ClassChild ref={classChildEl} />
    </div>
  );
};

Function组件中,使用callback ref的方式使用子组件的ref

一般什么情况下会使用这种方式来调用ref

例如:子组件中需要暴露ref,存在延迟加载的情况,就需要使用这种方式。useRef会在组件初始化时就获取ref,但是初始化时,可能子组件需要暴露ref的节点还没有被渲染。具体 demo 可以查看:demo

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}

useImperativeHandle

Function组件中,暴露ref给父组件使用

由于Function组件没有实例,所以要使用useImperativeHandle,声明自身的ref。声明了以后就可以使用useRef获取到Function子组件的ref

function FnChild(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));

  return <input ref={inputRef} type="text" />;
}
FnChild = forwardRef(FnChild);

useEffect

useEffect会使函数延迟执行,也就是说React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect

执行的特点:

  • 执行的时机在 dom 渲染之后
  • 通过requestIdleCallback异步执行

具体使用方式

只在初始化时执行一次,通过第二个参数传入空数组实现
可以实现componentDidMount的调用

useEffect(() => {
  // code
}, []);

在初始化时执行一次,当props.test发生变化时会继续触发
可以实现componentDidUpdate的调用
如果要获取 pre 参数,可以参考实现 getDerivedStateFromProp 章节

useEffect(() => {
  // code
}, [props.test]);

useEffect返回一个函数可以实现componentWillUnmount

useEffect(() => {
  // code
  return () => {
    // code
  };
}, []);

ps:

  1. componentDidMountcomponentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。想了解关于useEffect的更多使用细节可查看官方文件说明
  2. 第二个参数,传入数组中的每一个值也会使用Object.is进行比较。如果返回true则不会触发。以及后续要介绍的类似用法的hooks也是这样。

useLayoutEffect

其使用方式与useEffect相同,区别只是调用时机不同。

不同点是:

  • 执行的时机在dom渲染之前
  • 同步执行,会阻塞dom的渲染

官方示例:例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)

个人理解:如果dom的渲染依赖某些初始化的计算,useLayoutEffect可以规避掉,使用useEffect异步计算然后更新dom造成的页面抖动的情况。

useCallback

返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。

类似vuecomputed属性的概念,只不过它是能够缓存某个方法,并且监听某些数据,如果这些数据没有改变,则useCallback返回的总是那个被缓存的方法(并且缓存当时的作用域中的变量值),只有当监听的数据发生变化才会更新作用域中的变量,并返回一个新的函数。

下面看一个例子来理解一下

index.jsx

// index.jsx
import React, { useState, useCallback } from "react";
import Button from "./button";

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

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

  const handleClickButton2 = useCallback(() => {
    setCount2(count2 + 1);
  }, [count2]);

  return (
    <div>
      <div>
        <Button type="1" onClickButton={handleClickButton1}>
          Button1
        </Button>
      </div>
      <div>
        <Button type="2" onClickButton={handleClickButton2}>
          Button2
        </Button>
      </div>
    </div>
  );
}

Button.jsx

// Button.jsx
import React from "react";
const map = {};
const Button = ({ onClickButton, type, children }) => {
  console.log(type, map[type] === onClickButton);
  map[type] = onClickButton;
  return (
    <>
      <button onClick={onClickButton}>{children}</button>
      <span>{Math.random()}</span>
    </>
  );
};

export default React.memo(Button);
// export default Button

第一种情况传入[count2]
效果是:

  • 点击handleClickButton1时,只会更新触发 button1 的 update,因为,因为只有handleClickButton1发生了变化
  • 点击handleClickButton2时,会触发 button1 和 button2 的 update,因为,count2 更新了,所以触发了handleClickButton2生成了一个新的函数
const handleClickButton2 = useCallback(() => {
  setCount2(count2 + 1);
}, [count1]);

第二种情况传入[]
效果是:

  • 点击handleClickButton1时,只会更新触发 button1 的 update,因为,因为只有handleClickButton1发生了变化
  • 点击handleClickButton2时,只有第一次点击时会触发 button2 的 update,[]意思是只有第一次初始化时会生成一个handleClickButton2,后续触发的一直是原来的那个handleClickButton2,同时handleClickButton2的作用域会被缓存起来,所以每次点击其实都是触发setCount2(0 + 1);
const handleClickButton2 = useCallback(() => {
  setCount2(count2 + 1);
}, []);

第二种情况传入[count1]
效果是:

  • 点击handleClickButton1时,只会更新触发 button1 的 update,因为,因为只有handleClickButton1发生了变化
  • 点击handleClickButton2时,第一次点击时会触发 button2 的 update,后续如果要触发 button2 的 update,就必须先点击handleClickButton1,触发 count1 的变化。
const handleClickButton2 = useCallback(() => {
  setCount2(count2 + 1);
}, [count1]);

useMemo

传递一个创建函数和依赖项,创建函数会需要返回一个值,只有在依赖项发生改变的时候,才会重新调用此函数,返回一个新的值。

看完了useCallback,再来看useMemo,就很好理解了,useMemo会返回一个值,并监听某些值的变化,只有当监听的值发生变化,才会触发useMemo的重新计算,其实它就是vuecomputed这个apireact版本。

注意:

由于useMemo还不是很稳定,后期可能会修改,所以官方建议如果某个值从不需要被重新计算,可以惰性初始化 一个值

  1. 初始化创建一个昂贵的state
// bad
function Table(props) {
  // ⚠️ createRows() 每次渲染都会被调用
  const [rows, setRows] = useState(createRows(props.count));
  // ...
}

// good
function Table(props) {
  // ✅ createRows() 只会被调用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}
  1. 避免重新创建 useRef() 的初始值
// bad
function Image(props) {
  // ⚠️ IntersectionObserver 在每次渲染都会被创建
  const ref = useRef(new IntersectionObserver(onIntersect));
  // ...
}

// good
function Image(props) {
  const ref = useRef(null);

  // ✅ IntersectionObserver 只会被惰性创建一次
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }
  // 当你需要时,调用 getObserver()
  // ...
}

useContext

Function组件实现context跨层级组件通信的解决方案。

下面是一个官方的demo

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

同时Function组件也可以这样使用context,但是会增加组件层级的嵌套

import React from "react";
const Context = React.createContext();
const Provider = Context.Provider;
const Consumer = Context.Consumer;

const store = {
  userInfo: {
    userId: 1,
    userName: "smw"
  }
};

function MyContext(props) {
  return (
    <Provider value={store}>
      <div>
        Context
        <OneLevel />
      </div>
    </Provider>
  );
}

function OneLevel(props) {
  return (
    <div>
      OneLevel
      <Consumer>{ctx => <TwoLevel {...ctx} />}</Consumer>
    </div>
  );
}

Hooks 使用技巧

实现 getDerivedStateFromProps

在使用hooks时,我们可以这样实现getDerivedStateFromProps

例如:当父组件传递过来的userId发生变化时,我们需要重新获取userInfo,来更新视图。

function UserInfo({ userId }) {
  const [userInfo, setUserInfo] = useState(() => {
    const userInfo = getUserInfoByUserId(userId);
    setPrevUserId(userInfo);
  });
  const [prevUserId, setPrevUserId] = useState(userId);

  if (userId !== prevUserId) {
    // 获取新的userId,去获取数据
    const userInfo = getUserInfoByUserId(userId);
    setPrevUserId(userId);
  }

  return (
    <div>
      <p>userName:{userInfo.userName}</p>
    </div>
  );
}

以上是官方推荐的实现方法,但是你要单独创建一个state用来保存preProps,如果你只需要监听变化,而不要用到preProps,个人认为以下方式实现起来更简单明了,useEffect在初始化的时候会执行一次,同时当userId发生变化也会触发。满足getDerivedStateFromProps的触发规则。

const UserInfo = ({ userId }) => {
  const [userInfo, setUserInfo] = useState({});

  useEffect(() => {
    const userInfo = userInfoMap[userId];
    setUserInfo(userInfo);
  }, [userId]);

  return (
    <div>
      <p>name : {userInfo.userName}</p>
    </div>
  );
};

实现 shouldComponentUpdate

可以用 React.memo 包裹一个组件来对它的 props 进行浅比较

  • 等效于PureComponent,但是只比较 props
  • 自定义props比较逻辑
const MemoChild = React.memo(Child, (prevProps, nextProps) => {
  // 比较prevProps和nextProps
  // 返回true则不更新
  return true;
});

如何避免向下传递回调

如果一个子组件需要调用父父组件的方法,需要将这个方法使用props一层层向下传递。写起来很麻烦。

React官方推荐通过context,用useReducer往下传递一个dispatch函数

const TodosDispatch = React.createContext(null);

function TodosApp() {
  // 提示:`dispatch` 不会在重新渲染之间变化
  const [todos, dispatch] = useReducer(todosReducer);

  return (
    <TodosDispatch.Provider value={dispatch}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  );
}

欢迎关注微信公众号
wxys.png


嘻哈工程师
50 声望2 粉丝

前端工程师 php nodejs