React中为何无法通过Ref存储的resolve函数兑现Promise?
问题:自定义promisify Hook无法在状态更新后兑现Promise
我尝试创建一个promisify风格的Hook,目标是结合Redux Toolkit使用React 19的use Hook。以下是该Hook的核心实现:
function usePromise(input: { isLoading: true, data: null } | { isLoading: false, data: string; }) { // 存储resolve函数到ref中 const resRef = useRef<(data: string) => void>(null); // 创建新Promise并将resolve函数存入ref const promiseRef = useRef( new Promise((res) => { resRef.current = res; // res("xxx") // 👈 这样调用可以正常resolve }) ); // 当input变化时,如果已有数据则兑现Promise useEffect(() => { if (!input.isLoading) { resRef.current?.(input.data); } }, [input]); // 返回Promise实例 return promiseRef.current; }
使用示例如下:
export function MyComponent() { const [value, setValue] = useState<null | string>(null); const prom = usePromise(value ? { isLoading: false, data: value } : { isLoading: true, data: null }); prom.then((v) => alert(v)) return <div> <button onClick={() => setValue("123")}>Click me</button> </div> }
我预期点击按钮后,Promise会被兑现并弹出alert,但实际并未生效。
问题原因与解决方案
核心原因
- 渲染阶段执行副作用违规:你在组件的渲染阶段直接调用
prom.then(...)执行alert这类副作用,违反了React渲染阶段必须是纯函数的规则。React会对渲染阶段的代码进行优化或批处理,导致回调无法按预期触发。 - 回调注册时机错误:每次组件渲染都会重新调用
prom.then(...)注册新回调,当Promise最终被resolve时,这些在渲染阶段注册的回调执行时机被React的渲染机制干扰,导致alert无法正常弹出。
修复方案
方案1:将副作用移至useEffect中
把then的逻辑放到useEffect里,确保副作用在组件挂载或依赖变化时执行:
export function MyComponent() { const [value, setValue] = useState<null | string>(null); const prom = usePromise(value ? { isLoading: false, data: value } : { isLoading: true, data: null }); useEffect(() => { prom.then((v) => alert(v)); }, [prom]); return <div> <button onClick={() => setValue("123")}>Click me</button> </div> }
方案2:结合React 19的use Hook使用(符合你的原始目标)
React 19的use Hook专门用于在渲染阶段读取Promise的值,配合useEffect处理副作用:
export function MyComponent() { const [value, setValue] = useState<null | string>(null); const prom = usePromise(value ? { isLoading: false, data: value } : { isLoading: true, data: null }); // 使用React 19的use Hook读取Promise结果 const data = use(prom); // 当数据可用时执行alert useEffect(() => { alert(data); }, [data]); return <div> <button onClick={() => setValue("123")}>Click me</button> </div> }
额外优化:支持重复加载场景
原Hook的Promise实例在组件生命周期内唯一,无法支持“加载完成后再次进入加载状态”的场景。如果需要重复加载,可在input.isLoading为true时重新创建Promise:
function usePromise(input: { isLoading: true, data: null } | { isLoading: false, data: string; }) { const resRef = useRef<(data: string) => void>(null); const promiseRef = useRef<Promise<string>>(); // 进入加载状态时,重新创建新的Promise if (input.isLoading) { promiseRef.current = new Promise((res) => { resRef.current = res; }); } useEffect(() => { if (!input.isLoading && promiseRef.current) { resRef.current?.(input.data); } }, [input]); // 兜底返回已resolve的Promise,避免undefined return promiseRef.current ?? Promise.resolve(""); }
内容的提问来源于stack exchange,提问作者dwjohnston
相关产品推荐
相关产品推荐

