前言
Hook 让你可以在组件中使用不同的 React 功能,目前React官网一共列出了15个内置Hook,本文主要就是围绕这15个Hook的用法及使用场景进行介绍。
状态流转
useState
声明一个你可以直接更新的 state 变量。
【语法】
react
// 这里可以任意命名,因为返回的是数组,数组解构
const [state, setState] = useState(initialState);
【使用示例】
react
// 声明一个名为count的状态变量,初始值为0
const [count, setCount] = useState(0);
useEffect的详细用法可以看这一篇 👉 《简单易懂的React状态管理Hook useState详解》
useReducer
在组件的顶层作用域调用 useReducer 以创建一个用于管理状态的 reducer。
useReducer可以让你将状态更新的逻辑抽象成一个纯函数,这样可以提高代码的可读性和可维护性。也可以配合useContext来实现类似Redux的全局状态管理。
【语法】
jsx
// state 当前的状态
// dispatch 一个函数,可以用来更新状态和触发重新渲染
const [state, dispatch] = useReducer(reducer, initialArg, init?)
reducer
:你自定义的状态逻辑函数,参数为 state 和 action。用于更新 state ,返回值是更新后的 stateinitialArg
:初始化 state ,可以是一个简单的值,也可以是一个对象。init?
:计算初始值的函数。如果存在,使用 init(initialArg) 的执行结果作为初始值,否则使用 initialArg。
【使用示例】
实现一个计数器应用
react
function reducer(state, action) {
switch (action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
return state;
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
使用场景
- 组件中有复杂的状态逻辑
- 有多个子值的状态对象
- 想要优化性能避免不必要的渲染时
useContext
useContext 可以让你在组件中读取和订阅一个 context 的值。context 是一种在组件树中传递数据的方式,不需要通过 props。
简单的说,就是useContext 可以让你在不同层级的组件之间共享数据,而不需要一层层地传递 props。例如,你可以用 context 来传递主题色、语言、用户信息等。
【语法】
react
const value = useContext(SomeContext)
-
SomeContext
:一个对象,由React.createContext
返回。这个值由距离组件最近的SomeContext.Provider
的value prop
决定。如果没有对应的Provider
,那么返回值就是创建该context
时传入的defaultValue
。 -
value
:返回值
【使用示例】
javascript
// 创建一个 context
const SomeContext = React.createContext(defaultValue);
// 在组件中使用 useContext 读取 context 的值
function MyComponent() {
const value = useContext(SomeContext);
// ...
}
// 在组件树中提供 context 的值
function MyPage() {
return (
<SomeContext.Provider value={someValue}>
<MyComponent />
</SomeContext.Provider>
);
}
操作节点
useRef
当你需要在组件中操作 DOM 元素,或者保存一些不随渲染变化的值的时候可以用useRef。
例如,你可以用 ref 来保存一个计时器的 ID、一个表单的数据、一个回调函数等。
【语法】
react
const ref = useRef(initialValue)
initialValue
:初始值ref
:返回值,一个包含 current 属性的对象。- 这个 current 属性的初始值就是传入的初始值,你可以随时修改它,但是修改它不会触发组件的重新渲染。
- 如果你把这个 ref 对象作为 JSX 元素的 ref 属性,那么 React 就会把这个元素的实例赋给 current 属性。在后续的渲染中,useRef 会返回同一个对象。
【使用示例】
javascript
// 创建一个 ref
const someRef = useRef(initialValue);
// 在组件中使用 ref 访问 DOM 元素
function MyComponent() {
const inputRef = useRef(null);
// ...
return <input ref={inputRef} />;
}
// 在组件中使用 ref 保存一个值
function MyTimer() {
const timerIdRef = useRef(0);
// ...
function startTimer() {
timerIdRef.current = setInterval(() => {
// ...
}, 1000);
}
function stopTimer() {
clearInterval(timerIdRef.current);
}
}
通过使用 ref,你可以确保:
- 你可以在重新渲染之间 存储信息(不像是普通对象,每次渲染都会重置)。
- 改变它 不会触发重新渲染(不像是 state 变量,会触发重新渲染)。
- 对于你的组件的每个副本来说,这些信息都是本地的(不像是外面的变量,是共享的)。
注意:改变 ref 不会触发重新渲染,所以 ref 不适合用于存储期望显示在屏幕上的信息。
useImperativeHandle
useImperativeHandle 可以让你在使用 ref 时有更多的控制权,你可以自己决定子组件向父组件暴露什么。这样可以避免父组件过多地依赖子组件的细节,也可以保护子组件的内部状态。
不过,useImperativeHandle 应该尽量少用,因为它会破坏组件的封装性,增加代码的复杂度。
【语法】
react
useImperativeHandle(ref, createHandle, dependencies?)
ref
:一个 ref 对象createHandle
:一个函数,可以在这个函数中返回任何你想要暴露给父组件的方法或属性,这个返回值父组件可以通过 ref.current 访问到值。dependencies?
:用来指定什么时候更新 ref 的值
【使用示例】
javascript
// 在子组件中使用 useImperativeHandle 定义 ref 的值
function MyChildComponent(props, ref) {
// ...
useImperativeHandle(ref, () => ({
// 返回一个对象,包含子组件想要暴露给父组件的方法或属性
focus: () => {
// ...
},
getValue: () => {
// ...
},
}));
// ...
}
// 使用 forwardRef 包裹子组件,使其能够接收 ref
const MyChildComponent = React.forwardRef(MyChildComponent);
// 在父组件中使用 ref 访问子组件的实例值
function MyParentComponent() {
const childRef = useRef(null);
// ...
return <MyChildComponent ref={childRef} />;
}
处理副作用
useEffect
由渲染引起的副作用。每当组件渲染时,React 将更新屏幕,然后运行 useEffect 中的代码。
【语法】
react
useEffect(setup, dependencies?)
【使用示例】
react
useEffect(() => {
if (isPlaying) {
// ...
} else {
// ...
}
}, [isPlaying]); // ......所以它必须在此处声明!
useEffect的详细用法可以看这一篇 👉 《React小白进阶之useEffect和ref》
useLayoutEffect
useLayoutEffect可以在DOM更新后同步地执行一些副作用操作。
【语法】
react
useLayoutEffect(setup, dependencies?)
它的用法和useEffect类似,都接受一个函数作为参数,并且可以返回一个清理函数。(注意:useLayoutEffect 可能会影响性能。尽可能使用 useEffect)
【使用示例】
jsx
import React, { useState, useEffect, useLayoutEffect } from "react";
function App() {
const [count, setCount] = useState(0);
// useEffect会在页面渲染后执行
useEffect(() => {
if (count === 0) {
// 设置count为随机数
setCount(Math.floor(Math.random() * 100));
}
}, [count]);
// useLayoutEffect会在页面渲染前执行
useLayoutEffect(() => {
if (count === 0) {
// 设置count为随机数
setCount(Math.floor(Math.random() * 100));
}
}, [count]);
return (
<div className="App">
<h1>useLayoutEffect vs useEffect</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
export default App;
如果运行这个代码,你会发现useEffect和useLayoutEffect的效果是不一样的。当你点击Reset按钮时,
- useEffect会先显示0,然后再显示一个随机数,因为它是在页面渲染后才执行的。
- useLayoutEffect会直接显示一个随机数,因为它是在页面渲染前就执行了,所以不会出现闪烁的效果。
【useLayoutEffect和useEffect的不同点】
- useLayoutEffect会在浏览器绘制页面之前执行
- useEffect会在浏览器绘制页面之后执行
【使用场景区分】
- 【useLayoutEffect】:如果你的副作用操作需要读取或修改DOM的布局,或者需要保证在页面渲染前就执行,比如获取元素的尺寸或位置,或者触发强制同步的重新渲染。那么就用useLayoutEffect。
- 【useEffect】:如果你的副作用操作不涉及DOM的布局,或者可以延迟到页面渲染后再执行,比如发送网络请求或设置定时器。则用useEffect。
useInsertionEffect
useInsertionEffect 是为 CSS-in-JS 库的作者特意打造的。除非你正在使用 CSS-in-JS 库并且需要注入样式,否则你应该使用 useEffect 或者 useLayoutEffect
【语法】
scss
useInsertionEffect(setup, dependencies?)
【使用示例】
jsx
import { useInsertionEffect } from 'react';
// 在你的 CSS-in-JS 库中
function useCSS(rule) {
useInsertionEffect(() => {
// ... 在此注入 <style> 标签 ...
});
return rule;
}
性能优化
useMemo
useMemo可以用来优化函数组件的性能。useMemo的作用是记住一个值,只有当它的依赖项发生变化时,才会重新计算这个值。这样可以避免在每次渲染时都执行一些复杂或开销大的计算。
【语法】
javascript
const cachedValue = useMemo(calculateValue, dependencies)
- calculateValue:是一个函数,它返回要记住的值
- dependencies:是一个数组,它指定了依赖项。如果依赖项数组为空,那么只会在组件挂载时执行一次函数,并且永远不会重新计算。
【使用示例】
jsx
import React, { useState, useMemo } from 'react';
function App() {
const [count, setCount] = useState(0); // 用于计数的状态
const [name, setName] = useState(''); // 用于输入姓名的状态
// 使用useMemo记住一个对象,只有当name变化时才重新创建
const person = useMemo(() => ({ name }), [name]);
return (
<div>
<h1>useMemo示例</h1>
<p>你好,{person.name}!</p>
<input value={name} onChange={e => setName(e.target.value)} />
<p>你点击了{count}次。</p>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
);
}
export default App;
以上代码,当你点击按钮时,person对象的引用不会变化,因为它被useMemo记住了。这样可以避免触发不必要的渲染或副作用,比如如果你需要把person对象作为props传递给子组件。
【useMemo使用场景总结】
- 需要对一个值进行复杂或开销大的计算,而这个值又只依赖于某些状态时,你可以使用useMemo来记住这个值,避免在每次渲染时都重新计算。
- 需要把一个值作为props传递给子组件时,你可以使用useMemo来保证这个值的引用不变,避免子组件因为接收到新的props而重新渲染。
- 需要创建一个对象或数组时,你可以使用useMemo来保证这个对象或数组的引用不变,避免触发不必要的渲染或副作用。
useCallback
useCallback可以让你在组件中创建一个只有在依赖项变化时才会更新的函数。useCallback的作用是优化性能,避免不必要的渲染和重新计算。
【语法】
react
const cachedFn = useCallback(fn, dependencies)
fn
:想要缓存的函数。可以接受任何参数并且返回任何值。dependencies
:有关是否更新fn
的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。cachedFn
:返回值,返回你已经传入的fn
函数
【使用示例】
jsx
import React, { useState, useCallback } from 'react';
import Child from './Child';
function Parent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<Child increment={increment} />
</div>
);
}
在这个例子中,Parent组件有一个count状态和一个increment函数,它们都被传递给了Child组件。increment函数使用了useCallback,它只有在第一次渲染时才会创建,之后就会一直保持不变,除非依赖项发生变化。这样就保证了Child组件在接收到increment函数时,不会因为它的引用变化而重新渲染。
useMemo和useCallback的区别
- useMemo返回的是一个值
- useCallback返回的是一个函数。(useCallback也可以用来优化性能,特别是当需要把函数作为props传递给子组件时,可以避免不必要的重新渲染。)
useTransition
它可以让你在不阻塞UI的情况下更新状态,帮助你实现一些并发模式的功能,比如:在数据加载时显示一个加载指示器,或者在路由切换时显示一个过渡动画。
【语法】
react
const [isPending, startTransition] = useTransition()
isPending
:一个布尔值,表示是否有一个过渡正在进行中。startTransition
:一个函数,用于将一个状态更新标记为过渡。
【使用示例】
jsx
import { useTransition, useState } from 'react';
function App() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
const increment = () => {
startTransition(() => {
setCount(c => c + 1);
});
}
return (
<div>
{isPending ? (
'Loading...'
) : (
<div>{count}</div>
)}
<button onClick={increment}>+</button>
</div>
);
}
在上面例子中,点击按钮时,通过 startTransition() 标记一个更新将要开始。此时 isPending 会变为 true,显示 Loading 状态。等到 setCount 完成更新,isPending 变为 false,组件退出 pending 状态,显示更新后的 count 值。
【主要使用场景】
- 列表中的项添加/删除时,平滑过渡显示新的列表状态。
- UI 组件切换时,实现组件之间平滑过渡。
- 数据加载时,显示加载状态过渡。
useDeferredValue
useDeferredValue它可以用来延迟值的更新,让UI更流畅。
在 React 中,当状态或属性发生变化时,组件会重新渲染。有时候,某些状态的变化并不会立即影响组件的显示,而是需要一些时间。例如,当用户在输入框中输入文字时,每次按键都会触发状态的更新,但实际上并不需要立即重新渲染组件,而是在用户停止输入一段时间后才需要更新显示。
这时候就可以使用 useDeferredValue
来延迟状态的更新。
【语法】
react
const deferredValue = useDeferredValue(value)
value
:你想延迟的值,可以是任何类型。
【使用示例】
假设有一个搜索框的组件,用户每次输入都会触发搜索功能。我们可以使用 useDeferredValue 来延迟搜索词的更新,以避免频繁的搜索请求
react
import { useState, useDeferredValue } from 'react';
function SearchBox() {
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);
// 模拟搜索请求的函数
const performSearch = async (term) => {
// 发起搜索请求
// ...
};
// 当 deferredSearchTerm 更新时,执行搜索
useEffect(() => {
performSearch(deferredSearchTerm);
}, [deferredSearchTerm]);
const handleChange = (event) => {
setSearchTerm(event.target.value);
};
return (
<input type="text" value={searchTerm} onChange={handleChange} />
);
}
其他Hook
这些 Hook 主要对库的作者有用,在应用代码中并不常用。
useDebugValue
允许你在 React 开发者工具中为自定义 Hook 添加一个标签。
scss
useDebugValue(value, format?)
useId
允许组件绑定一个唯一 ID。通常与可访问性 API 一起使用。
ini
const id = useId()
useSyncExternalStore
允许一个组件订阅一个外部 store。
ini
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
🎨【点赞】【关注】不迷路,更多前端干货等你解锁
往期推荐