最近在关注 React 19 的变化,比如 use、ref、useOptimistic 乐观更新等。
- use:请求接口,请求等待中,显示 loading,请求成功,展示数据。接收一个 Promise,会阻塞 render 继续渲染,需要配套 <Suspense> 处理 loading,配套 <ErrorBoundary> 来处理异常。
- ref 支持返回一个 cleanup 函数,在组件卸载时会调用该函数。
- useOptimistic 乐观更新:收藏点赞的场景,用户触发后,立即更新 UI,如果请求失败后,再回滚 UI。
use
use 是 React 19 新增的一个特性,支持处理 Promise 和 Context。
假如我们要实现这样一个需求:请求接口数据,请求过程中,显示 loading,请求成功,展示数据。
以前我们可能会这样写代码
function ReactUseDemo() {
const [data, setData] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
getList()
.then((res) => {
setData(res);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return <div>{data}</div>;
}
通过 use 我们可以把代码改造成下面这样
export default function ReactUseDemo() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ChildCompont />
</Suspense>
);
}
function ChildCompont() {
const data = use(getData());
return <div>{data}</div>;
}
use 接收一个 Promise,会阻塞 render 继续渲染,通常需要配套 Suspense 处理 loading 状态,需要配套 ErrorBoundary 来处理异常状态。
另外 use 也支持接收 Context,类似之前的 useContext,但比 useContext 更灵活,可以在条件语句和循环中使用。
function MyPage() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
);
}
function Form() {
const theme = use(ThemeContext);
......
}
use 的使用有一些注意事项
- 需要在组件或 Hook 内部使用
- use 可以在条件语句(比如 if)或者循环(比如 for)里面调用
ref
在之前,父组件传递 ref 给子组件,子组件如果要消费,则必须通过 forwardRef 来消费。
function RefDemo() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<Input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</div>
);
}
const Input = forwardRef((props, ref) => {
return <input ref={ref} />;
});
React 19 开始,不需要使用 forwardRef 了,ref 可以作为一个普通的 props 了。
export const Input = ({ ref }) => {
return <input ref={ref} />;
};
ref 支持返回 cleanup 函数
ref 支持返回一个 cleanup 函数,在组件卸载时会调用该函数。
<input
ref={(ref) => {
// ref created
// NEW: return a cleanup function to reset
// the ref when element is removed from DOM.
return () => {
// ref cleanup
};
}}
/>
useOptimistic 乐观更新
乐观更新是一种常见的体验优化手段,在发送异步请求之前,我们默认请求是成功的,让用户立即看到成功后的状态。
先来看看官方提供的例子:提交表单更新 name,可以立即将新的 name 更新到 UI 中。请求成功则 UI 不变,请求失败则 UI 回滚。
function ChangeName() {
const [name, setName] = useState("");
// 定义乐观更新的状态
const [optimisticName, setOptimisticName] = useOptimistic(name);
const submitAction = async (formData) => {
const newName = formData.get("name");
// 请求之前,先把状态更新到 optimisticLike
setOptimisticName(newName);
try {
await updateName(newName);
// 成功之后,更新最终状态
setName(newName);
} catch (e) {
console.error(e);
}
};
return (
<form action={submitAction}>
<p>Your name is: {optimisticName}</p>
<p>
<label>Change Name:</label>
<input type="text" name="name" disabled={name !== optimisticName} />
</p>
</form>
);
}
useOptimistic 用来维护临时状态,保证 UI 的乐观更新。
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
返回参数含义:
optimisticState
:乐观更新的状态,UI 上应该始终消费这个状态。默认等于真正的 state。addOptimistic
:更新 optimisticState,可以通过 updateFn 指定更新逻辑
传入参数含义:
state
:真正的状态updateFn
:(currentState, optimisticValue) => newOptimisticState
,调用 addOptimistic 的时候会通过这个函数生成新的 optimisticState
异步函数执行结束后,无论是成功还是失败,optimisticName 都会重置成和最新 state 一样。
关于乐观更新,我在日常开发中,经常会用到。经典的场景是收藏点赞场景,用户触发后,立即更新 UI,如果请求失败后,再回滚 UI。
function LikeDemo() {
const [like, setLike] = useState(false);
const [pending, startTransition] = useTransition();
const [optimisticLike, setOptimisticLike] = useOptimistic(like);
const handleLike = () => {
const targetLike = !like;
startTransition(async () => {
try {
setOptimisticLike(targetLike);
await updateLike(like);
setLike(targetLike);
} catch (e) {
console.error(e);
}
});
};
return (
<div>
<div onClick={handleLike}>{optimisticLike ? "收藏" : "未收藏"}</div>
</div>
);
}
其实我觉得这个 Hook 没多大用,我普通代码也可以实现这个乐观更新,而且更简单。
const [like, setLike] = useState(false);
const handleLike = async () => {
try {
setLike((s) => !s);
await updateLike(like);
} catch (e) {
setLike((s) => !s);
}
};