Hooks ——以 use 开头的函数——只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块。
hooks介绍视频:React Today and Tomorrow - Sophie Alpert and Dan Abramov - React Conf 2018 - YouTube
useState
useState Hook 提供了这两个功能:
- State 变量 用于保存渲染间的数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
const [index, setIndex] = useState(0);
这里的 [ 和 ] 语法称为数组解构,它允许你从数组中读取值。 useState 返回的数组总是正好有两项。
useState 的唯一参数是 state 变量的初始值。在这个例子中,index 的初始值被useState(0)设置为 0。
set函数
useState 返回的 set 函数允许你将 state 更新为不同的值并触发重新渲染。你可以直接传递新状态表达式,也可以传递一个根据先前状态来计算新状态的更新函数。
- 当传递一个新的状态表达式时,当状态更新后会直接去更新Dom;
- 当传递一个更新函数时,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 变量是通过调用 useStateHook 来声明的。
- Hook 是以 use开头的特殊函数。它们能让你 “hook” 到像 state 这样的 React 特性中。
- Hook 可能会让你想起 import:它们需要在非条件语句中调用。调用 Hook 时,包括 useState,仅在组件或另一个 Hook 的顶层被调用才有效。
- useStateHook 返回一对值:当前 state 和更新它的函数。
- 你可以拥有多个 state 变量。在内部,React 按顺序匹配它们。
- State 是组件私有的。如果你在两个地方渲染它,则每个副本都有独属于自己的 state。
useImmer
useImmer 是一个 React 自定义 Hook,它是由第三方库 use-immer 提供的。它的作用是简化在 React 中管理可变状态的过程,特别是在使用不可变数据结构的场景下。useImmer 可以帮助我们通过 draft(草稿)的方式来修改状态,并自动处理不可变性的更新。
主要的特点和优势:
- 简化状态更新: 使用 useImmer可以让我们在修改状态时更加简单和自然,而无需手动处理不可变性。你可以直接修改 draft(草稿)对象,就像在普通 JavaScript 中修改普通对象一样。
- 避免不可变性操作: 通过 useImmer,我们可以直接修改 draft 对象,而不用担心破坏原始状态的不可变性。useImmer内部会处理这些修改,并确保返回一个新的不可变状态,以便在适当的时候更新 React 组件的状态。
- 易读易写: 使用 useImmer使得状态更新更容易阅读和编写,并且更符合直觉。它让我们可以以更直观的方式来表达状态更新的逻辑,而无需深入了解不可变数据结构的细节。
- 支持嵌套对象和数组: 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 个参数:
- 一个 reducer 函数
- 一个初始的 state
它返回如下内容:
- 一个有状态的值
- 一个 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 对比
| ref | state | 
|---|---|
| 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的区别
createRef 和 useRef 都用于在 React 组件中创建对 DOM 元素或其他引用对象的引用,但它们在使用方式和一些情况下的行为上有一些区别。
 createRef 和 useRef 都用于在 React 组件中创建对 DOM 元素或其他引用对象的引用,但它们在使用方式和一些情况下的行为上有一些区别。
- 
创建时机: - createRef: 在类组件中使用。通过- this.myRef = React.createRef();创建引用。
- useRef: 在函数组件中使用。通过- const myRef = useRef();创建引用。
 
- 
持久性: - createRef: 每次组件渲染都会创建新的引用对象。适用于需要在每次渲染时重新创建引用的情况。
- useRef: 在组件的整个生命周期内保持不变。适用于需要在多次渲染之间保持引用对象不变的情况,例如用于保存状态的引用。
 
- 
访问引用: - createRef: 使用- this.myRef.current访问引用的 DOM 元素或组件实例。
- useRef: 使用- myRef.current访问引用的 DOM 元素或其他引用对象。
 
- 
对比引用值变化: - createRef: 无法直接监听引用值的变化,需要在- componentDidUpdate等生命周期方法中手动比较。
- useRef: 可以使用- ===操作符直接比较引用的值是否发生变化。
 
- 
更多特性: - createRef: 主要用于获取 DOM 元素的引用,不提供额外的特性。
- useRef: 除了用于获取 DOM 元素的引用外,还可以在函数组件中模拟类组件的实例变量,以及配合其他 React 钩子实现各种功能。
 
useEffect
你需要向 useEffect 传递两个参数:
- 一个 setup 函数 ,其 setup 代码 用来连接到该系统。 它应该返回一个 清理函数(cleanup),其 cleanup 代码 用来与该系统断开连接。
- 一个 依赖项列表,包括这些函数使用的每个组件内的值。
React 在必要时会调用 setup 和 cleanup,这可能会发生多次:
- 
将组件挂载到页面时,将运行 setup 代码。 
- 
重新渲染 依赖项 变更的组件后: - 首先,使用旧的 props 和 state 运行 cleanup 代码。
- 然后,使用新的 props 和 state 运行 setup 代码。
 
- 
当组件从页面卸载后,cleanup 代码 将运行最后一次。 
对应不同的生命周期
当依赖项为空时,分别可以对应componentDidMount 和 componentWillUnmount。
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
- 你不必使用 Effect 来转换渲染所需的数据
- 你不必使用 Effect 来处理用户事件
当你不确定某些代码应该放在 Effect 中还是事件处理函数中时,先自问 为什么 要执行这些代码。Effect 只用来执行那些显示给用户时组件 需要执行 的代码。
当你决定将某些逻辑放入事件处理函数还是 Effect 中时,你需要回答的主要问题是:从用户的角度来看它是 怎样的逻辑。如果这个逻辑是由某个特定的交互引起的,请将它保留在相应的事件处理函数中。如果是由用户在屏幕上 看到 组件时引起的,请将它保留在 Effect 中。
useLayoutEffect
useLayoutEffect 是 useEffect 的一个版本,在浏览器重新绘制屏幕之前触发。它可以在浏览器重新绘制屏幕前计算布局。
常见用法:
- 渲染初始的内容。
- 在 浏览器重新绘制屏幕之前 测量布局。
- 使用所读取的布局信息渲染最终内容。
useMemo
useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。
const cachedValue = useMemo(calculateValue, dependencies)
useCallback
useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。
const cachedFn = useCallback(fn, dependencies)
什么时候用useCallback
- 将函数作为 prop 传递给子组件: 当你将一个函数作为 prop 传递给子组件时,如果这个函数在每次渲染时都被重新创建,子组件可能会因为 prop 变化而触发不必要的重新渲染。通过使用 useCallback来记忆这个函数,可以确保只有在其依赖变化时才会重新创建。
- 作为依赖项传递给 useEffect: 当你在useEffect中使用函数作为依赖项时,如果函数在每次渲染时都被重新创建,可能会导致useEffect在不必要的情况下被触发。通过使用useCallback,你可以确保在函数依赖不变的情况下避免触发不必要的副作用。
- 优化内联函数: 在 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;
}