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