解决React Suspense中Await导致函数重复执行的问题
问题场景
在React中使用Suspense组件配合React-Router-DOM的Await组件等待loader返回的Promise解析时,遇到navigate、showAlert这类副作用函数被执行两次的问题。原始代码如下:
原始Loader代码
export const Loader = async ({ params }) => { CheckAuth(); let fetch_data = fetchApi('auth/activate-account', 'POST', { verify_token: params.verify_token }); return defer({ loaderPromise: fetch_data }); } // 该Loader在Route的loader属性中调用
原始组件代码
const VerifyToken = () => { const loaderData = useLoaderData(); const navigate = useNavigate(); const { showAlert } = useGeneral(); return ( <React.Suspense fallback={<PreLoader />}> <Await resolve={loaderData.loaderPromise}> {(resolvedData) => { if (resolvedData.type === "warning") { showAlert(resolvedData.type, resolvedData.msg); navigate('/Auth/Login', { replace: true }); return null; } return ( {/* 正常渲染的JSX */} ); }} </Await> </React.Suspense> ); };
核心原因
- React严格模式(开发环境)会对组件渲染、回调函数进行双重调用,用于检测潜在的副作用问题,而
Await的回调属于渲染阶段执行的函数,因此会被触发两次。 Suspense的重新渲染逻辑也可能导致回调重复执行。
解决方案
方案一:将副作用逻辑移到Loader中(推荐)
把跳转、弹窗这类副作用从组件渲染阶段移到路由的loader中处理,符合React Router的设计规范,从根源避免重复执行。
修改后的Loader代码
export const Loader = async ({ params }) => { CheckAuth(); const resolvedData = await fetchApi('auth/activate-account', 'POST', { verify_token: params.verify_token }); if (resolvedData.type === "warning") { // 将弹窗信息存入sessionStorage,供组件读取 sessionStorage.setItem('alertData', JSON.stringify({ type: resolvedData.type, msg: resolvedData.msg })); // 抛出重定向,由React Router直接处理跳转 throw redirect({ pathname: '/Auth/Login', replace: true }); } return defer({ loaderPromise: Promise.resolve(resolvedData) }); }
修改后的组件代码
const VerifyToken = () => { const loaderData = useLoaderData(); const { showAlert } = useGeneral(); // 组件挂载时读取弹窗信息并显示 React.useEffect(() => { const alertData = sessionStorage.getItem('alertData'); if (alertData) { const { type, msg } = JSON.parse(alertData); showAlert(type, msg); sessionStorage.removeItem('alertData'); } }, [showAlert]); return ( <React.Suspense fallback={<PreLoader />}> <Await resolve={loaderData.loaderPromise}> {(resolvedData) => { return ( {/* 正常渲染的JSX */} ); }} </Await> </React.Suspense> ); };
方案二:用useEffect包裹副作用逻辑
如果不想修改loader,可以将Await回调中的副作用移到useEffect中,利用状态存储解析后的数据,确保副作用只执行一次。
const VerifyToken = () => { const loaderData = useLoaderData(); const navigate = useNavigate(); const { showAlert } = useGeneral(); const [resolvedData, setResolvedData] = React.useState(null); // 处理副作用逻辑 React.useEffect(() => { if (resolvedData?.type === "warning") { showAlert(resolvedData.type, resolvedData.msg); navigate('/Auth/Login', { replace: true }); } }, [resolvedData, navigate, showAlert]); return ( <React.Suspense fallback={<PreLoader />}> <Await resolve={loaderData.loaderPromise}> {(data) => { setResolvedData(data); // 仅渲染正常状态的JSX if (data.type !== "warning") { return ( {/* 正常渲染的JSX */} ); } return null; }} </Await> </React.Suspense> ); };
方案三:优化useRef的实现方式
如果坚持使用原有的useRef方案,可以添加组件卸载时的清理逻辑,避免状态残留影响后续页面访问。
const VerifyToken = () => { const loaderData = useLoaderData(); const navigate = useNavigate(); const { showAlert } = useGeneral(); const hasExecuted = React.useRef(false); // 组件卸载时重置ref状态 React.useEffect(() => { return () => { hasExecuted.current = false; }; }, []); return ( <React.Suspense fallback={<PreLoader />}> <Await resolve={loaderData.loaderPromise}> {(resolvedData) => { if (resolvedData.type === "warning" && !hasExecuted.current) { hasExecuted.current = true; showAlert(resolvedData.type, resolvedData.msg); navigate('/Auth/Login', { replace: true }); return null; } return ( {/* 正常渲染的JSX */} ); }} </Await> </React.Suspense> ); };
内容的提问来源于stack exchange,提问作者Faraz Rabbani
相关产品推荐
相关产品推荐

