React Hooks
React Hooks 是 React 16.8 版本中引入的一种新功能,它允许在不编写类的情况下使用状态和生命周期等特性。
在 Hooks 出现之前,React 里的函数式组件也被称为无状态组件。
函数组件和类组件的区别?
- 类组件必须要注意
this
指向的问题,可能涉及到手动绑定的问题。而函数组件中不需要担心这个问题。 - 函数通过
useState
和useReducer
来管理状态,可以直接从函数的作用域内访问和修改变量,不需要this.state
和this.setState
- 函数组件没有生命周期,可以通过使用
useEffect
等 Hook 来模拟生命周期。而类组件有明确的生命周期方法。 - 函数组件因为不需要继承自
React.Component
并实现render
方法,也不需要写this
相关代码(例如:this.state
、this.props
),所以代码更加简洁。
useState
作用:在函数组件中使用状态。
语法:
ts
export const Count: React.FC = () => {
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1);
};
return (
<>
<h2>count的值是:{count}</h2>
<button onClick={add}>+1</button>
</>
);
};
常见面试题
-
useState 的初始状态可以是一个函数吗?
可以。如果是一个函数,React 会调用这个函数来计算初始值。
-
useState 是同步还是异步?
状态读取是同步的,状态更新是异步的。
在更新状态的时候,React 不会立即更新状态和渲染组件,它会将更新放入到队列中,并在之后批量处理,提高性能减少不必要的渲染。如果需要在状态变化后立即执行操作,可以使用 useEffect 来监听状态变化。
-
修改状态时,新值依赖于旧值,不要直接修改状态,而是需要通过函数来拿到值再计算。
tssetCount(count + 1); setCount((prev) => prev + 1);
useRef
作用:通常有两个
- 获取 DOM 元素或子组件的实例对象。
- 保存数据,且不触发重新渲染。
语法:
ts
export const InputComponent: React.FC = () => {
const inpRef = useRef<HTMLInputElement>(null);
const getFocus = () => {
inpRef.current?.focus();
};
return (
<>
<h2>useRef</h2>
<p>
<input type="text" aria-label="input" ref={inpRef} />
<span onClick={getFocus}>让input获取焦点</span>
</p>
</>
);
};
注意:
- useRef 会返回一个对象,他只有一个 current 的属性,并且可以存储任何类型的值。
- 更新 ref.current 的值不会造成组件的重新渲染。
- 组件重新渲染的时候 useRef 不会被重复初始化,一直都是第一次初始化时候的值。
- fef.curent 的值不能作为其他 Hooks 的依赖项(比如:useEffect 的依赖项,因为 useEffect 监听不到它的变化)。
常见面试题
- ref 如何获取函数式组件?
ts
// 首先要用React.forwardRef将子元素包括起来
export const Child = React.forwardRef((_, ref) => {
const [count, setCount] = useState(0);
const increase = () => {
setCount(count + 1);
};
const reduce = () => {
setCount(count - 1);
};
// 再把需要暴露给其他组件的状态通过useImperativeHandle这个Hook暴露出去
useImperativeHandle(ref, () => ({
count,
}));
return (
<>
<h2>count的值是:{count}</h2>
<button onClick={increase}>+1</button>
<button onClick={reduce}>-1</button>
</>
);
});
// 最后这里就可以获取到其他函数组件的状态
export const Father: React.FC = () => {
const ChildRef = useRef();
const getChild = () => {
console.log(ChildRef.current);
};
return (
<>
<button onClick={getChild}>获取儿子</button>
<Child ref={ChildRef} />
</>
);
};
useImperativeHandle
作用:用于配合 useRef 和 React.forwardRef 来获取其他函数组件暴露的状态和方法。
语法:
ts
export const Child = React.forwardRef((_, ref) => {
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1);
};
useImperativeHandle(
ref,
() => {
console.log("执行了 useImperativeHandle 的回调");
return {
count,
setCount,
};
},
[]
);
return (
<>
<h2>count的值是:{count}</h2>
<button onClick={add}>+1</button>
</>
);
});
参数:
- 父组件传递过来的 ref。
- 回调函数,暴露给父组件的状态和方法
- 依赖项
- 依赖项为空数组:就代表回调函数只在组件首次渲染完毕之后执行一次,向外 return 一个 ref。当组件再次渲染的时候不会再触发回调函数的执行。
- 依赖项为指定状态(代码示例中为 count):就代表回调函数在 count 变化的时候就会触发。
- 省略依赖项:就代表组件内任何状态的变化都会触发回调函数的执行。
useEffect
作用:useEffect 是 React 中的一个 Hook,它允许在函数组件中执行副作用操作。在类组件中,这些操作通常放在 componentDidMount、componentDidUpdate 和 componentWillUnmount 生命周期方法中,但在函数组件中,你可以使用 useEffect 来处理这些场景。
语法:
ts
export const User: React.FC = () => {
const [username, setUsername] = useState("张三");
const [age, setAge] = useState(0);
useEffect(() => {
console.log("useEffect");
}, [age]);
return (
<>
<h2>用户名:{username}</h2>
<button onClick={() => setUsername("李四")}>修改用户名</button>
<h2>年龄:{age}</h2>
<button onClick={() => setAge(age + 1)}>增加年龄</button>
</>
);
};
参数:
- 回调函数
- 依赖项
- 依赖项为空数组:就代表回调函数只在组件首次渲染完毕之后执行一次。当组件再次渲染的时候不会再触发回调函数的执行。
- 依赖项为指定状态(代码示例中为 age):就代表回调函数在组件首次渲染的时候和 age 变化的时候就会触发,其他状态变化则不会触发。
- 省略依赖项:就代表回调函数在组件首次渲染的时候和任何状态的变化都会触发回调函数的执行。
注意:
- 不要在 useEffect 中改变依赖项的值,会造成死循环。(因为状态变化会触发页面更新,页面更新会触发 useEffect 的执行,而在 useEffect 中又更新了状态,状态变化会触发页面更新...)。如果非要在 useEffect 中更新状态,就必须要把它第二个参数设置为一个空数组。
- 多个不同功能的副作用尽量分开声明,不要写到一个 useEffect 中。
常见面试题
- useEffect 的主要用途是什么?
useEffect 是一个 Hook,允许在函数组件中执行副作用操作,如数据获取、订阅或手动 DOM 操作。它类似于类组件中的生命周期方法 componentDidMount、componentDidUpdate 和 componentWillUnmount。 - 什么是函数的副作用?
函数的副作用就是函数除了返回值以外,指定的其他操作。也就是与组件渲染无关的操作。例如获取数据、修改全局变量、操作 DOM 、设置和清理定时器等。 - 如何阻止 useEffect 在首次渲染时执行?
可以在 useEffect 内部增加条件判断来实现。比如判断某个依赖项是否为初始值,如果是就不执行回调函数,反之则调用。 - 当依赖项是一个复杂对象或函数时,useEffect 如何处理?
当依赖项是复杂对象或函数时,应该用 useMemo 或 useCallback 来确保这些依赖项在没有实际变化时不发生改变,这样可以防止不必要的 useEffect 触发。这是因为 useEffect 使用浅比较来检测依赖项的变化。 - 在 useEffect 中如何清理副作用?
useEffect 可以返回一个函数,用于清理副作用的回调。语法如下:
注意事项:-
return 出去的函数是 React 组件自动调用的。
-
执行顺序是:每次执行副作用函数的时候,先查看有没有 return 出去的函数,如果有就先执行它,然后再执行自己的副作用操作(组件被卸载的时候也会执行清理函数)。
实际使用场景:
-
清除网络请求。
-
当前组件中使用了定时器绑定了事件监听,可以在返回的函数中清理定时器或解绑事件监听。
-
自定义 Hooks
作用:自定义 hook 函数,可以将某些组件逻辑提取到可重用的函数中。
注意:
- 命名规范:建议命名规则 useXxx。
- 调用:不要在循环、条件或嵌套函数中调用 Hook。
- 避免手动管理状态:尽量利用 React 提供的状态管理工具(如 useState 和 useReducer)。
ts
export const useFetch = (url: string, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 创建 AbortController 实例,用于取消请求
const controller = new AbortController();
const { signal } = controller;
const fetchData = async () => {
try {
const response = await fetch(url, { ...options, signal });
if (!response.ok) {
throw new Error("Network response was not ok");
}
const result = await response.json();
setData(result);
} catch (err) {
console.log(err);
setData(null);
} finally {
setLoading(false);
}
};
fetchData();
// 清理逻辑:取消未完成的请求
return () => {
controller.abort();
};
}, [url, options]); // 确保当 url 或 options 发生变化时重新发起请求
return { data, loading };
};
useLayoutEffect
作用:useLayoutEffect 和 useEffect 的使用方法相似。
语法:
ts
import React, { useRef, useLayoutEffect } from "react";
function MeasureExample() {
const ref = useRef();
useLayoutEffect(() => {
console.log("Element height:", ref.current.offsetHeight);
});
return <div ref={ref}>Hello World!</div>;
}
export default MeasureExample;
区别:
hooks 名称 | 执行时机 | 执行过程 |
---|---|---|
useEffect | 组件渲染之后 | 异步执行,不阻塞浏览器绘制 |
useLayoutEffect | 组件渲染之前(将要渲染时) | 同步执行,阻塞浏览器绘制(建议不要执行过于复杂的逻辑,否则会造成页面卡顿) |
常见使用场景: |
- 当需要在组件渲染后立即获取 DOM 节点的尺寸、位置等信息时,应该使用 useLayoutEffect。因为此时 DOM 已经更新,但是还没有绘制,你可以安全地读取这些属性而不会引起额外的布局重排。
- 如果在 useEffect 中修改了影响布局的样式或属性,这可能会导致页面先显示旧布局,然后突然调整到新布局,造成视觉上的抖动。通过将这些操作移到 useLayoutEffect 中,可以在浏览器绘制之前完成变更,避免不必要的闪烁或不连贯的用户体验。
useReducer
作用:当状态更新逻辑比较复杂的时候可以考虑使用 useReducer,它可以同时更新多个状态。
功能演示:
ts
import React, { useReducer } from "react";
import "./style.css";
type UserType = typeof initState;
type ActionType =
| { type: "UPDATE_NAME"; payload: string }
| { type: "INCREMENT"; payload: number }
| { type: "DECREMENT"; payload: number }
| { type: "RESET" };
// 初始状态
const initState = { name: "张三", age: -1 };
// 预处理函数
// 形参是初始状态,返回的是处理好的初始状态
// 在该示例中,如果年龄小于1岁,则将年龄设置为1岁,因为年龄不能小于1
const initAction = (prevState: UserType) => {
return {
...initState,
age: prevState.age <= 0 ? 1 : prevState.age,
};
};
// 在reducer函数的形参中,第一个参数永远都是上一次的旧状态
const reducer = (prevState: UserType, action: ActionType) => {
console.log("触发了 reducer 函数");
console.log(action);
switch (action.type) {
case "UPDATE_NAME":
return {
...prevState,
name: action.payload,
};
case "INCREMENT":
return {
...prevState,
age: prevState.age + action.payload,
};
case "DECREMENT":
return {
...prevState,
age: prevState.age - action.payload,
};
case "RESET":
return {
...initState,
age: initState.age <= 0 ? 1 : initState.age,
};
default:
return prevState;
}
// 在reducer函数中,必须返回一个新的状态(这个新的状态会作为下一次的旧状态)
// return prevState;
};
export const Father: React.FC = () => {
const [state, dispatch] = useReducer(reducer, initState, initAction);
console.log(state);
const onChangeUserName = () => {
// 注意:不可以直接修改state数据
// state.name = "张三丰";
dispatch({
type: "UPDATE_NAME", // 操作类型
payload: "张三丰", // 修改数据的值
});
};
return (
<>
<button onClick={onChangeUserName}>修改用户名</button>
<p>{JSON.stringify(state)}</p>
<div className="father">
<Son1 {...state} dispatch={dispatch} />
<Son2 {...state} dispatch={dispatch} />
</div>
</>
);
};
const Son1: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (
props
) => {
const { dispatch, ...user } = props;
const add = () => {
dispatch({
type: "INCREMENT",
payload: 1,
});
};
return (
<div className="son1">
<p>{JSON.stringify(user)}</p>
<button onClick={add}>增加年龄</button>
</div>
);
};
const Son2: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (
props
) => {
const { dispatch, ...user } = props;
const dec = () => {
dispatch({
type: "DECREMENT",
payload: 1,
});
};
return (
<div className="son2">
<p>{JSON.stringify(user)}</p>
<button onClick={dec}>减少年龄</button>
<hr />
<Grandson dispatch={dispatch} />
</div>
);
};
const Grandson: React.FC<{ dispatch: React.Dispatch<ActionType> }> = (
props
) => {
const { dispatch } = props;
const reset = () => {
dispatch({
type: "RESET",
});
};
return (
<div>
<p>Grandson components</p>
<button onClick={reset}>重置</button>
</div>
);
};
使用 Immer 让 useReducer 更简洁:
shell
npm i immer use-immer -S
ts
import React from "react";
import { useImmerReducer } from "use-immer";
import "./style.css";
type UserType = typeof initState;
type ActionType =
| { type: "UPDATE_NAME"; payload: string }
| { type: "DECREMENT"; payload: number };
const initState = { name: "张三", age: -1 };
const initAction = (prevState: UserType) => {
return {
...initState,
age: prevState.age <= 0 ? 1 : prevState.age,
};
};
const reducer = (prevState: UserType, action: ActionType) => {
switch (action.type) {
case "UPDATE_NAME":
prevState.name = action.payload;
break;
case "DECREMENT":
prevState.age -= action.payload;
break;
default:
return prevState;
}
};
export const Father: React.FC = () => {
const [state, dispatch] = useImmerReducer(reducer, initState, initAction);
console.log(state);
const onChangeUserName = () => {
dispatch({
type: "UPDATE_NAME",
payload: "张三丰",
});
};
return (
<>
<button onClick={onChangeUserName}>修改用户名</button>
<p>{JSON.stringify(state)}</p>
<div className="father">
<Son2 {...state} dispatch={dispatch} />
</div>
</>
);
};
const Son2: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (
props
) => {
const { dispatch, ...user } = props;
const dec = () => {
dispatch({
type: "DECREMENT",
payload: 1,
});
};
return (
<div className="son2">
<p>{JSON.stringify(user)}</p>
<button onClick={dec}>减少年龄</button>
</div>
);
};
常见面试题
- useReducer 和 useState 的主要区别是什么?
useState 适合简单状态的管理,状态更新逻辑相对直接。
useReducer 更适合处理复杂的状态逻辑,特别是当状态变化依赖于前一个状态或需要处理一系列相关联的状态时。useReducer 将状态和状态的变化统一管理在 reducer 函数里面,使得状态转换逻辑更加清晰。 - 在什么情况下应该选择 useReducer 而不是 useState?
当组件的状态是一个对象或数组,并且状态转换逻辑复杂时。
当多个状态之间相互关联,改变一个状态的同时也需要改变另一个状态时。
useContext
作用:实现多层组件时间的数据共享。简化组件之间的数据传递,尤其是在深层嵌套组件的情况下。
功能演示:
ts
import React, { useState, useContext } from "react";
type AppContextType = {
count: number;
user: { name: string; age: number };
setCount: React.Dispatch<React.SetStateAction<number>>;
setUser: React.Dispatch<React.SetStateAction<{ name: string; age: number }>>;
};
// 步骤1. 在全局创建Context对象
const AppContext = React.createContext<AppContextType>({} as AppContextType);
export const Father: React.FC = () => {
const [user, setUser] = useState({ name: "TJD", age: 21 });
const [count, setCount] = useState(0);
const onChangeName = () => {
setUser({
...user,
name: "土鸡蛋",
});
};
return (
// 步骤2. 在父组件使用Provider包裹子组件,并传入value
<AppContext.Provider value={{ user, count, setCount, setUser }}>
<h1>father</h1>
<p>count:{count}</p>
<button onClick={onChangeName}>change name</button>
<Child1 />
</AppContext.Provider>
);
};
const Child1: React.FC = () => {
// 步骤3. 在子组件中使用useContext获取Context对象
const { user, setUser } = useContext(AppContext);
const onChangeAge = () => {
setUser({
...user,
age: 22,
});
};
return (
<div>
<h2>child</h2>
<p>name:{user.name}</p>
<p>age:{user.age}</p>
<button onClick={onChangeAge}>change age</button>
<GrandChild />
</div>
);
};
注意:在实际使用场景中,可以将 AppContext 包裹一下,这样可以解决组件代码的侵入问题。
常见面试题
- useContext 是否只能在顶层使用?
useContext 必须在函数组件的顶层调用,不能在条件语句、循环或者嵌套函数内部调用。这是因为在每次渲染时,React 需要知道哪些组件订阅了 Context,以便于在 Context 的值发生变化时通知它们。 - useContext 是否会导致组件重新渲染?
当 useContext 提供的 value 发生改变时,所有使用 useContext 订阅该 context 的组件都会触发重新渲染。可以通过使用 React.memo、useMemo、useCallback 来防止不必要的渲染。
React.memo 函数
作用:用于性能优化,避免不必要的重新渲染。在实际开发中,如果某个子组件只依赖于 props 的变化而重新渲染,就可以使用 React.mome()将其包裹起来。
功能演示:
ts
import React, { useState } from "react";
export const Father: React.FC = () => {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
return (
<>
<h2>父组件</h2>
<p>count:{count}</p>
<p>flag:{flag.toString()}</p>
<button onClick={() => setCount(count + 1)}>+ 1</button>
<button onClick={() => setFlag(!flag)}>Toggle</button>
<hr />
<Son count={count} />
</>
);
};
const Son: React.FC<{ count: number }> = React.memo(({ count }) => {
console.log("Son render");
return (
<>
<h3>子组件</h3>
<p>子组件count:{count}</p>
</>
);
});
常见面试题
- React.memo 是如何工作的?
它会对新旧 props 进行浅比较,如果 props 没有变化,则跳过子组件的重新渲染,直接复用之前的渲染结果。 - React.memo 和 useMemo 或 useCallback 有什么区别?
- React.memo 是一个高阶组件,用来包裹整个函数组件,以防止组件在 props 没有变化时重新渲染。
- useMemo 和 useCallback 是 Hooks,分别用来缓存计算结果和回调函数,以避免不必要的计算或传递新的函数引用给子组件。
useMemo
作用:避免不必要的计算。如果有一个函数组件内的值是通过复杂计算得到的,可以使用 useMemo 来确保这个计算只会在依赖项改变时执行。
语法:
ts
const memoCount = useMemo(() => {}, []);
参数:
- 回调函数:处理计算结果,并将计算的结果 return 出去。
- 依赖项:
- 依赖项为空数组:只会计算一次。
- 依赖项为指定状态数组:只有依赖项发生变化的时候会重新计算。
- 省略依赖项:每次更新都会重新计算。
功能演示:
ts
// 该示例中,只有flag的依赖项发生变化的时候才会触发计算,count变化不会触发。
import React, { useState, useMemo } from "react";
export const Father: React.FC = () => {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const tips = useMemo(() => {
console.log("tips render");
return flag ? <p>真的</p> : <p>假的</p>;
}, [flag]);
return (
<>
<h2>父组件</h2>
<p>count:{count}</p>
<p>flag:{flag.toString()}</p>
{tips}
<button onClick={() => setCount(count + 1)}>+ 1</button>
<button onClick={() => setFlag(!flag)}>Toggle</button>
<hr />
<Son count={count} />
</>
);
};
const Son: React.FC<{ count: number }> = React.memo(({ count }) => {
console.log("Son render");
return (
<>
<h3>子组件</h3>
<p>子组件count:{count}</p>
</>
);
});
常见面试题
- useMemo 的主要作用是什么?
用于缓存计算结果,只有在依赖项发生变化时才会重新计算,从而避免不必要的昂贵计算和渲染。 - useMemo 和 useCallback 有什么区别?
- useMemo 用来缓存值或对象,比如数组、对象或者计算得出的结果。
- useCallback 是用来缓存函数,它返回一个记忆化的函数,该函数不会因为组件的重新渲染而改变引用,除非其依赖项发生了变化。
- useMemo 和 React.memo 有什么不同?
- useMemo 是用来记忆计算结果或函数,适用于函数内部的优化。
- React.memo 是一个高阶组件,用于包裹整个函数组件,以避免当 props 没有变化时的不必要重渲染。
useCallback
作用:对组件内部的函数进行缓存,返回的是缓存函数。
语法:
ts
const memoCallback = useCallback(() => {}, []);
参数:
- 回调函数:用户处理业务逻辑,这个函数就是要被缓存的函数。
- 依赖项:
- 依赖项为空数组:只会在组件第一次渲染的时候计算一次。
- 依赖项为指定状态数组:依赖项发生变化的时候,重新计算。
- 省略依赖数组:每次更新都会重新计算。
功能演示:
ts
import React, { useState, useEffect, useCallback } from "react";
type SearchInputType = {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
type wordType = {
id: number;
word: string;
};
export const Father: React.FC = () => {
const [key, setKey] = useState("");
// 这里的依赖项为空数组,这个函数只需要执行一次,因为onKeyChange不需要依赖其他状态变化而重新执行
const onKeyChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setKey(e.target.value);
}, []);
return (
<>
<SearchInput onChange={onKeyChange} />
<SearchResult query={key} />
</>
);
};
const SearchInput: React.FC<SearchInputType> = React.memo((props) => {
useEffect(() => {
console.log("SearchInput 被渲染了");
});
return (
<>
<input type="text" aria-label="search" onChange={props.onChange} />
</>
);
});
const SearchResult: React.FC<{ query: string }> = (props) => {
const [list, setList] = useState([]);
useEffect(() => {
if (!props.query) return setList([]);
fetch("https://api.test.com/v1/words?kw=" + props.query).then((res) => {
res.json().then((data) => {
setList(data.data);
});
});
}, [props.query]);
return (
<>
<ul>
{list.map((item: wordType) => (
<li key={item.id}>{item.word}</li>
))}
</ul>
</>
);
};
常见面试题
- useCallback 的主要作用是什么?
useCallback 是一个 React Hook,用于缓存回调函数。它确保只有在依赖项发生变化时才会返回一个新的函数,从而保持函数引用的稳定性,避免不必要的子组件重新渲染,提高性能。 - useCallback 和 useMemo 有什么缺点?
使用 useCallback 或 useMemo 都会在每次渲染时增加额外的逻辑来检查依赖项的变化。如果依赖项频繁变化,或者计算本身并不昂贵,那么这些 Hooks 可能不会带来显著的性能提升,反而增加了额外的复杂性。
useTransition
作用:将耗时的操作(如数据获取、复杂计算等)标记为"过渡态",也就是这些被标记的可以延迟执行,以确保更紧急的用户交互(如点击、滚动等)能够优先得到响应。
语法:
ts
import { useTransition } from "react";
const Container: React.FC = () => {
const [isPending, startTransition] = useTransition();
// ...
};
参数:useTransition 不需要传递参数,直接调用就可以。
返回值(数组):
- isPending 布尔值:是否存在待处理的 transition,如果为 true,说明页面上存在待渲染的部门,可以给用户一个加载提示。
- startTransition 函数:调用此函数,可以把状态的更新标记为低优先级,不阻塞 UI 对用户操作的响应。
使用场景: - 数据获取:需要从服务器获取数据时,可以使用 useTransition 来确保数据获取操作不会阻塞用户的交互。
- 复杂计算:如果你的应用中包含复杂的计算逻辑,可以使用 useTransition 来确保这些计算不会影响应用的响应性
- 界面更新:对于那些不需要立即反映在界面上的状态更新,可以使用 useTransition 来延迟这些更新,保证界面交互的流畅性。
注意事项: - 传递给 startTransition 的函数必须是同步的。因为 React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition,如果在其执行期间尝试稍后更新状态(比如在定时器中更新状态),这些状态更新则不会被标记为 transition。
- 标记为 transition 的状态更新被其他状态更新打断,优先执行其他的。
- transition 更新不能用于控制文本输入,因为它会导致中间状态的丢失(因为被标记为低优先级的更新状态,后面的状态更新会终止掉前面的渲染。其实更适合于 tab 标签页切换的优化)。
代码示例:
ts
/** 功能:切换home、movie、about三个页面,movie页面比较耗时,利用useTransition将页面切换优先级降低,并且如果该页面存在待处理显示loading */
import { FC, PropsWithChildren, useState, useTransition } from "react";
import "./style.css";
// 父组件
export const TabsContainer: FC = () => {
const [activeTab, setActiveTab] = useState("home");
const [isPending, startTransition] = useTransition();
// 按钮的点击事件处理函数
const onBtnClick = (name: string) => {
// 把某次更新,标记为低优先级的,从而防止页面卡顿的情况
startTransition(() => {
// 更新状态,将当前激活的标签页设置点击的
setActiveTab(name);
});
};
return (
<div style={{ height: 500 }}>
{/* 按钮区域 */}
<TabButton
isActive={activeTab === "home"}
onClick={() => onBtnClick("home")}
>
Home
</TabButton>
// ...
</div>
);
};
// ...更多代码暂为显示
useDeferredValue
作用:用于优化用户体验,特别是在处理频繁更新的状态时。它允许开发者将某些状态的更新标记为"可延迟的",从而让 React 知道这些更新可以被推迟,直到所有紧急的用户交互(如点击、滚动等)都被处理完毕。这有助于保持应用的响应性和流畅性。
简单来说:useDeferredValue 就像是给状态更新加了一个"慢一点"的按钮。它告诉 React 这个状态更新不着急,等用户做完其他更重要的事情(比如滚动页面、点击按钮)之后再处理。
参数:一个状态(就是用 useState 声明的状态)。
返回值:一个延迟版本的状态。
使用场景:
- 实时搜索框。用户每次输入字符时都会触发一次搜索请求。为了确保输入过程中的流畅性,你可以使用 useDeferredValue 来延迟显示搜索结果,
语法:
ts
import { useState, useDeferredValue } from "react";
export const SearchBox: FC = () => {
const [keyWord, setKeyWord] = useState("");
// 根据状态(keyWord)的变化,返回一个延迟版本的状态(keyWord)
const deferredKeyWord = useDeferredValue(keyWord);
};
功能演示:
常见面试题
- useDeferredValue 和防抖、节流的区别?
- 防抖:在用户停止输入一段时间之后再执行操作,需要手动设置间隔执行时间。
- 截流:每隔一段时间再执行操作,需要手动设置间隔执行时间。
- useDeferredValue:
不需要设置间隔执行时间,会根据设备性能自定判断。
如果页面正在渲染一个大型列表,但是用户进行了一次键盘输入,React 会放弃渲染,优先处理键盘输入。 - 总结:如果优化的工作不是渲染期间发生的,那么防抖和节流仍然是有用的(例如:减少网络请求等)。