React Hooks

56 阅读15分钟

Hooks ——以 use 开头的函数——只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块。

hooks介绍视频:React Today and Tomorrow - Sophie Alpert and Dan Abramov - React Conf 2018 - YouTube

useState

useState Hook 提供了这两个功能:

  1. State 变量 用于保存渲染间的数据。
  2. State setter 函数 更新变量并触发 React 再次渲染组件。
const [index, setIndex] = useState(0);

这里的 [] 语法称为数组解构,它允许你从数组中读取值。 useState 返回的数组总是正好有两项。

useState 的唯一参数是 state 变量的初始值。在这个例子中,index 的初始值被useState(0)设置为 0

set函数

useState 返回的 set 函数允许你将 state 更新为不同的值并触发重新渲染。你可以直接传递新状态表达式,也可以传递一个根据先前状态来计算新状态的更新函数。

  1. 当传递一个新的状态表达式时,当状态更新后会直接去更新Dom;
  2. 当传递一个更新函数时,React 将更新函数放入 队列 中。然后,在下一次渲染期间,它将按照相同的顺序调用它们,调用完之后再计算下一个状态,最后再去更新Dom;

查看官网示例

useState的核心代码

let componentHooks = [];
let currentHookIndex = 0;

function useState(initialState) {
  let pair = componentHooks[currentHookIndex];
  if (pair) {
    // 这不是第一次渲染
    // 所以 state pair 已经存在
    // 将其返回并为下一次 hook 的调用做准备
    currentHookIndex++;
    return pair;
  }

  // 这是我们第一次进行渲染
  // 所以新建一个 state pair 然后存储它
  pair = [initialState, setState];

  function setState(nextState) {
    // 当用户发起 state 的变更,
    // 把新的值放入 pair 中
    pair[0] = nextState;
    updateDOM();
  }

  // 存储这个 pair 用于将来的渲染
  // 并且为下一次 hook 的调用做准备
  componentHooks[currentHookIndex] = pair;
  currentHookIndex++;
  return pair;
}

总结

  • 当一个组件需要在多次渲染间“记住”某些信息时使用 state 变量。
  • State 变量是通过调用 useState Hook 来声明的。
  • Hook 是以 use 开头的特殊函数。它们能让你 “hook” 到像 state 这样的 React 特性中。
  • Hook 可能会让你想起 import:它们需要在非条件语句中调用。调用 Hook 时,包括 useState,仅在组件或另一个 Hook 的顶层被调用才有效。
  • useState Hook 返回一对值:当前 state 和更新它的函数。
  • 你可以拥有多个 state 变量。在内部,React 按顺序匹配它们。
  • State 是组件私有的。如果你在两个地方渲染它,则每个副本都有独属于自己的 state。

useImmer

useImmer 是一个 React 自定义 Hook,它是由第三方库 use-immer 提供的。它的作用是简化在 React 中管理可变状态的过程,特别是在使用不可变数据结构的场景下。useImmer 可以帮助我们通过 draft(草稿)的方式来修改状态,并自动处理不可变性的更新。

主要的特点和优势:

  1. 简化状态更新: 使用 useImmer 可以让我们在修改状态时更加简单和自然,而无需手动处理不可变性。你可以直接修改 draft(草稿)对象,就像在普通 JavaScript 中修改普通对象一样。
  2. 避免不可变性操作: 通过 useImmer,我们可以直接修改 draft 对象,而不用担心破坏原始状态的不可变性。useImmer 内部会处理这些修改,并确保返回一个新的不可变状态,以便在适当的时候更新 React 组件的状态。
  3. 易读易写: 使用 useImmer 使得状态更新更容易阅读和编写,并且更符合直觉。它让我们可以以更直观的方式来表达状态更新的逻辑,而无需深入了解不可变数据结构的细节。
  4. 支持嵌套对象和数组: useImmer 可以处理嵌套的对象和数组的状态更新。这对于复杂的数据结构来说非常有用,因为你可以在一个回调函数中处理整个数据结构的更新。

首先,需要导入 useImmer 自定义 Hook:

import { useImmer } from 'use-immer';

然后,使用 useImmer 来初始化状态,并得到状态和更新状态的函数:

const [state, updateState] = useImmer(initialState);

在事件处理程序或其他需要修改状态的地方,使用 updateState 来更新状态。在 updateState 中,你可以通过直接修改 draft 对象来更新状态,而不用担心不可变性的问题。

function handleButtonClick() {
  updateState(draft => {
    draft.count += 1;
  });
}

总结来说,useImmer 是一个方便而强大的 React 自定义 Hook,使得在 React 中管理可变状态变得更加简单和直观,同时避免了手动处理不可变性带来的繁琐问题。如果你喜欢在 React 中使用可变状态,但又希望避免不可变性的麻烦,useImmer 可能是一个很好的选择。

useReducer

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useReducer 钩子接受 2 个参数:

  1. 一个 reducer 函数
  2. 一个初始的 state

它返回如下内容:

  1. 一个有状态的值
  2. 一个 dispatch 函数(用来 “派发” 用户操作给 reducer)

对比 useState 和 useReducer

  • 代码体积:  通常,在使用 useState 时,一开始只需要编写少量代码。而 useReducer 必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer 可以减少代码量。
  • 可读性:  当状态更新逻辑足够简单时,useState 的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer 允许你将状态更新逻辑与事件处理程序分离开来。
  • 可调试性:  当使用 useState 出现问题时, 你很难发现具体原因以及为什么。 而使用 useReducer 时, 你可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个 action)。 如果所有 action 都没问题,你就知道问题出在了 reducer 本身的逻辑中。 然而,与使用 useState 相比,你必须单步执行更多的代码。
  • 可测试性:  reducer 是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和 action,断言 reducer 返回的特定状态会很有帮助。
  • 个人偏好:  并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。你可以随时在 useState 和 useReducer 之间切换,它们能做的事情是一样的!

useImmerReducer

我们可以使用 useImmerReducer 简化 reducers。useImmerReducer是第三方use-immer提供的自定义hook。

首先,需要导入 useImmerReducer 自定义 Hook:

import { useImmerReducer } from 'use-immer';

然后,使用 useImmerReducer 来初始化状态,并得到状态和更新状态的函数:

const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);

在编写reducer函数时,我们可以如下所示:

function tasksReducer(draft, action) {
  switch (action.type) {
    case 'added': {
      draft.push({
        id: action.id,
        text: action.text,
        done: false,
      });
      break;
    }
    case 'changed': {
      const index = draft.findIndex((t) => t.id === action.task.id);
      draft[index] = action.task;
      break;
    }
    case 'deleted': {
      return draft.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('未知 action:' + action.type);
    }
  }
}

Reducers 应该是纯净的,所以它们不应该去修改 state。而 Immer 为你提供了一种特殊的 draft 对象,你可以通过它安全的修改 state。在底层,Immer 会基于当前 state 创建一个副本。这就是为什么通过 useImmerReducer 来管理 reducers 时,可以修改第一个参数,且不需要返回一个新的 state 的原因。

useContext

useContext 可以让你读取和订阅组件中的 context。

// 创建一个Context

import React, { useContext } from 'react';

// 创建一个Context
const MyContext = React.createContext();

// 在组件中使用Provider包裹,使Context在整个组件树中生效
function App() {
  return (
    <MyContext.Provider value="Hello from Context!">
      <div>
        <h1>Using Context.Consumer and useContext</h1>
        <ConsumerComponent />
        <useContextComponent />
      </div>
    </MyContext.Provider>
  );
}

export default App;

之前我们使用 MyContext.Consumer 消费Context中的值。

// 使用Context.Consumer消费Context中的值
function ConsumerComponent() {
  return (
    <MyContext.Consumer>
      {value => (
        <div>
          <p>{value}</p>
        </div>
      )}
    </MyContext.Consumer>
  );
}

使用useContext钩子访问Context中的值

// 使用useContext钩子访问Context中的值
function useContextComponent() {
  const value = useContext(MyContext);

  return (
    <div>
      <p>{value}</p>
    </div>
  );
}

如果是同一个MyContext,无论是Context.Consumer 还是 useContext,都会从最近的 MyContext.Provider 的value中读取context

useRef

当你希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染 时,你可以使用 ref

const ref = useRef(0);

与 state 一样,ref 在重新渲染之间由 React 保留。但是,设置 state 会重新渲染组件,而更改 ref 不会!你可以通过 ref.current 属性访问该 ref 的当前值。

ref 是一个普通的 JavaScript 对象,具有可以被读取和修改的 current 属性。

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('你点击了 ' + ref.current + ' 次!');
  }

  return (
    <button onClick={handleClick}>
      点我!
    </button>
  );
}

ref 和 state 对比

refstate
useRef(initialValue)返回 { current: initialValue }useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数 ( [value, setValue])
更改时不会触发重新渲染更改时触发重新渲染。
可变 —— 你可以在渲染过程之外修改和更新 current 的值。“不可变” —— 你必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。
你不应在渲染期间读取(或写入) current 值。你可以随时读取 state。但是,每次渲染都有自己不变的 state 快照

useRef的实现

原则上 useRef 可以在 useState 的基础上 实现。 你可以想象在 React 内部,useRef 是这样实现的:

// React 内部
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}

何时使用 ref

通常,当你的组件需要“跳出” React 并与外部 API 通信时,你会用到 ref —— 通常是不会影响组件外观的浏览器 API。以下是这些罕见情况中的几个:

  • 存储 timeout ID
  • 存储和操作 DOM 元素
  • 存储不需要被用来计算 JSX 的其他对象。

useRef 和 createRef的区别

createRefuseRef 都用于在 React 组件中创建对 DOM 元素或其他引用对象的引用,但它们在使用方式和一些情况下的行为上有一些区别。

createRefuseRef 都用于在 React 组件中创建对 DOM 元素或其他引用对象的引用,但它们在使用方式和一些情况下的行为上有一些区别。

  1. 创建时机:

    • createRef: 在类组件中使用。通过 this.myRef = React.createRef(); 创建引用。
    • useRef: 在函数组件中使用。通过 const myRef = useRef(); 创建引用。
  2. 持久性:

    • createRef: 每次组件渲染都会创建新的引用对象。适用于需要在每次渲染时重新创建引用的情况。
    • useRef: 在组件的整个生命周期内保持不变。适用于需要在多次渲染之间保持引用对象不变的情况,例如用于保存状态的引用。
  3. 访问引用:

    • createRef: 使用 this.myRef.current 访问引用的 DOM 元素或组件实例。
    • useRef: 使用 myRef.current 访问引用的 DOM 元素或其他引用对象。
  4. 对比引用值变化:

    • createRef: 无法直接监听引用值的变化,需要在 componentDidUpdate 等生命周期方法中手动比较。
    • useRef: 可以使用 === 操作符直接比较引用的值是否发生变化。
  5. 更多特性:

    • createRef: 主要用于获取 DOM 元素的引用,不提供额外的特性。
    • useRef: 除了用于获取 DOM 元素的引用外,还可以在函数组件中模拟类组件的实例变量,以及配合其他 React 钩子实现各种功能。

useEffect

你需要向 useEffect 传递两个参数:

  1. 一个 setup 函数 ,其 setup 代码 用来连接到该系统。 它应该返回一个 清理函数(cleanup),其 cleanup 代码 用来与该系统断开连接。
  2. 一个 依赖项列表,包括这些函数使用的每个组件内的值。

React 在必要时会调用 setup 和 cleanup,这可能会发生多次

  1. 将组件挂载到页面时,将运行 setup 代码。

  2. 重新渲染 依赖项 变更的组件后:

    • 首先,使用旧的 props 和 state 运行 cleanup 代码。
    • 然后,使用新的 props 和 state 运行 setup 代码。
  3. 当组件从页面卸载后,cleanup 代码 将运行最后一次。

对应不同的生命周期

当依赖项为空时,分别可以对应componentDidMountcomponentWillUnmount

useEffect(() => {
  console.log('Component mounted'); // 在组件挂载后执行
  return () => {
    console.log('Component will unmount'); // 在组件卸载前执行
  };
}, []);

当不提供依赖数组时,每次组件更新时都执行,相当于 componentDidUpdate

useEffect(() => {
  console.log('Component updated'); // 在组件更新后执行
});

当有依赖项时,且当依赖项发生变化时,才会执行。

useEffect(() => {
  console.log('Component updated'); // 在组件更新后执行
}, [count]); // 仅在 count 发生变化时执行

什么时候不用useEffect

  1. 你不必使用 Effect 来转换渲染所需的数据
  2. 你不必使用 Effect 来处理用户事件

当你不确定某些代码应该放在 Effect 中还是事件处理函数中时,先自问 为什么 要执行这些代码。Effect 只用来执行那些显示给用户时组件 需要执行 的代码。

当你决定将某些逻辑放入事件处理函数还是 Effect 中时,你需要回答的主要问题是:从用户的角度来看它是 怎样的逻辑。如果这个逻辑是由某个特定的交互引起的,请将它保留在相应的事件处理函数中。如果是由用户在屏幕上 看到 组件时引起的,请将它保留在 Effect 中。

useLayoutEffect

useLayoutEffectuseEffect 的一个版本,在浏览器重新绘制屏幕之前触发。它可以在浏览器重新绘制屏幕前计算布局。

常见用法:

  1. 渲染初始的内容。
  2. 在 浏览器重新绘制屏幕之前 测量布局。
  3. 使用所读取的布局信息渲染最终内容。

useMemo

useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。

const cachedValue = useMemo(calculateValue, dependencies)

用法

useCallback

useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。

const cachedFn = useCallback(fn, dependencies)

用法

什么时候用useCallback

  1. 将函数作为 prop 传递给子组件: 当你将一个函数作为 prop 传递给子组件时,如果这个函数在每次渲染时都被重新创建,子组件可能会因为 prop 变化而触发不必要的重新渲染。通过使用 useCallback 来记忆这个函数,可以确保只有在其依赖变化时才会重新创建。
  2. 作为依赖项传递给 useEffect: 当你在 useEffect 中使用函数作为依赖项时,如果函数在每次渲染时都被重新创建,可能会导致 useEffect 在不必要的情况下被触发。通过使用 useCallback,你可以确保在函数依赖不变的情况下避免触发不必要的副作用。
  3. 优化内联函数: 在 JSX 中使用内联函数作为事件处理程序时,如果不使用 useCallback,每次渲染都会创建一个新的函数实例,可能会影响性能。通过使用 useCallback,你可以确保只在依赖变化时重新创建函数。

useCallback 与 useMemo 有何关系?

  • useMemo 缓存函数调用的结果
  • useCallback 缓存函数本身

useDeferredValue

useDeferredValue 是一个 React Hook,可以让你延迟更新 UI 的某些部分。

在组件的顶层调用 useDeferredValue 来获取该值的延迟版本。

import { useState, useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  // ...
}

用法

useId

useId 是一个 React Hook,可以生成传递给无障碍属性的唯一 ID。

import { useId } from 'react';

function PasswordField() {
  const passwordHintId = useId();
  // ...
}

用法

useImperativeHandle

useImperativeHandle 是一个用于自定义 React 组件实例的钩子函数。它允许你在函数组件内部向父组件暴露一些特定的实例方法,使得父组件可以通过 ref 访问和调用这些方法。

import { forwardRef, useImperativeHandle } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => {
    return {
      // ... 暴露给父组件的方法 ...
      focus() {
        inputRef.current.focus();
      },
      scrollIntoView() {
        inputRef.current.scrollIntoView();
      },
      
    };
  }, []);
 return <input {...props} ref={inputRef} />;
})

自定义hook

自定义 Hook 应该以 "use" 开头,这是为了告诉其他开发者这是一个钩子函数。

useDebounce

import { useState, useEffect, useRef } from 'react';

// 自定义 useDebounce 钩子

function useDebounce(callback, delay, dependencies = []) {
  const [result, setResult] = useState(null);
  const callbackRef = useRef(callback);
  
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      const callbackResult = callbackRef.current();
      setResult(callbackResult);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [...dependencies, delay]);

  return result;
}

useThrottle

import { useState, useEffect, useRef } from 'react';

// 自定义 useThrottle 钩子
function useThrottle(callback, delay, dependencies = []) {
  const [result, setResult] = useState(null);
  const [lastExecuted, setLastExecuted] = useState(0);
  const callbackRef = useRef(callback);

  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);
  
  useEffect(() => {
    const now = Date.now();
    if (now - lastExecuted >= delay) {
      const callbackResult = callbackRef.current();
      setResult(callbackResult);
      setLastExecuted(now);
    }
  }, [...dependencies, callback, delay, lastExecuted]);

  return result;
}