React hooks

本文主要记录常用 hooks 的用法和理解。

useState

useState 让函数组件也可以有记住 state 的能力。

useState 的用法

class 组件这么写:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: "Hello",
    };
  }
}

函数组件中使用 useState 这么写:

const App = () => {
  const [value, setValue] = useState("Hello");

  const callback = () => {
    // 调用后会导致重新渲染
    setValue('World');
  }
};

第二个参数 setValue 等于 Class 组件的 this.setState()

函数签名:

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

第二个参数 setState 说明

setState 可以传入值,也可以传入函数 ,若传入函数,将会得到参数为当前 state 值。

举例:

const handleCountAddByCallBack = useCallback(() => {
  setCount((count) => count + 1);
}, []);

useEffect

useEffect(官方文档)主要用来解决组件一些副作用的问题。比如组件渲染后从服务端取回用户名回显;修改网页的 title;发起一个 WebSocket 的链接。

useEffect 相当于 class 组件的 componentDidMountcomponentWillUnmount 的组合。

函数签名

// setup 可以是一个异步函数
function useEffect(setup: (() => undefined|() => void) | () => Promise<void|(() => void)>; dependencies?: ReadonlyArray<unknown>);

一个简单的例子

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
}

如何理解 useEffect 的参数呢?

第一个参数, setup

第二个参数,dependencies

如果不希望 useEffect 每次渲染都执行,这时可以使用它的第二个参数 dependencies ,使用一个数组指定副效应函数的依赖项,只有依赖项发生变化,才会重新渲染。

如下代码示例,传入 [serverUrl, roomId] 后只有在 serverUrl roomId变化时才会再次执行 setup 函数。

一个复杂的例子

import { useEffect } from "react";
import { createConnection } from "./chat.js";

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState("https://localhost:1234");

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);
  // ...
}

答疑解惑

useEffect 做了什么?
通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffect?
将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗?
是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。(我们稍后会谈到如何控制它。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

useRef

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useReducer

import { useReducer } from "react";

const initialState = { pageNum: 1, pageSize: 15 };

const reducer = (state, action) => {
  switch (action.type) {
    case "next": // 下一页
      return { ...state, pageNum: state.pageNum + 1 };
    case "prev": // 前一页
      return { ...state, pageNum: state.pageNum - 1 };
    case "changePage": // 跳转到某页
      return { ...state, pageNum: action.payload };
    case "changeSize": // 更改每页展示条目数
      return { pageNum: 1, pageSize: action.payload };
    default:
      return state;
  }
};

const Page = () => {
  const [pager, dispatch] = useReducer(reducer, initialState);

  return (
    <Table
      pageNum={pager.pageNum}
      pageSize={pager.pageSize}
      onGoNext={() => dispatch({ type: "next" })}
      onGoPrev={() => dispatch({ type: "prev" })}
      onPageNumChange={(num) => dispatch({ type: "changePage", payload: num })}
      onPageSizeChange={(size) =>
        dispatch({ type: "changeSize", payload: size })
      }
    />
  );
};

useContext

const value = useContext(MyContext);
import React, { createContext, useContext } from "react";

const ThemeContext = createContext({ theme: "light" });

function Button() {
  const { theme } = useContext(ThemeContext);
  return <div>current theme is {theme}</div>;
}

useContext + useReducer

一起使用:

import { createContext, useReducer, useContext } from "react";

const ParentDispatch = createContext(null);

const Parent = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <ParentDispatch.Provider value={dispatch}>
      <DeepTree parentState={state} />
    </ParentDispatch.Provider>
  );
};

// 深层子组件
const DeepChild = () => {
  const dispatch = useContext(ParentDispatch);

  const handleClick = () => {
    dispatch({ type: "add", payload: "hello" });
  };

  return <button onClick={handleClick}>Add</button>;
};

性能优化

useMemo

type DependencyList = ReadonlyArray<any>;
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;

useCallback

type DependencyList = ReadonlyArray<any>;
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

useMemo useCallback 区别

一句话: useCallback 缓存钩子函数,useMemo 缓存返回值(计算结果)

对比:

useContextuseReducer
返回值一个缓存的回调函数一个缓存的值
参数需要缓存的函数,依赖项需要缓存的值(也可以是个计算然后再返回值的函数) ,依赖项
使用场景父组件更新时,通过 props 传递给子组件的函数也会重新创建,然后这个时候使用 useCallBack 就可以缓存函数不使它重新创建组件更新时,一些计算量很大的值也有可能被重新计算,这个时候就可以使用 useMemo 直接使用上一次缓存的值

举个简单的例子

未使用 hooks

import React, { FC, useCallback, useMemo, useState } from 'react';

const Index: FC = (props) => {
  const [count, setCount] = useState(0);

  const isEvenNumber = count % 2 === 0;
  const onClick = () => setCount(count + 1);

  return (
    <div>
      <div>{count} is {isEvenNumber  'even':'odd'} number</div>
      <button onClick={onClick}></button>
    </div>
  );
};

使用 hooks

import React, { FC, useCallback, useMemo, useState } from 'react';

const Index: FC = (props) => {
  const [count, setCount] = useState(0);

  const isEvenNumber = useMemo(() => {
    return count % 2 === 0;
  }, [count]);

  const onClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <div>{count} is {isEvenNumber  'even':'odd'} number</div>
      <button onClick={onClick}></button>
    </div>
  );
};

为什么会有React Hooks 出现

React 的函数组件是非常好用的东西,相比 class 写法以及 Vue 的对象挂载写法简洁很多,代码测试复用成本低,容易入手,但也带来一些问题,无状态函数很理想,但现实有一些计算开销大、组件渲染频繁的场景是需要状态的,每次都计算一遍状态(callback 和 二次计算值)无疑很浪费内存,函数不像对象(React class 写法或者 Vue 组件写法)可以直接将状态挂载在自身,没有浪费内存的问题,要实现类似的效果只能找一个的内存挂载点挂载这些东东,所以有了 useCallback 和 useMemo 这些 hook。

编写属于你的第一个Hooks

基于React提供的内置 Hooks 开发即可。本质上是函数的嵌套。

// 定义
export const useDialog = () => {
  const [visible, setVisible] = useState(false);
  const show = () => setVisible(true);
  const hide = () => setVisible(false);
  return [visible, show, hide];
}

// 使用
function Staff() {
  const [visible, show, hide] = useDialog();
  // do staff ...
}

热门 React hooks 库

更多阅读