React18 StrictMode下useEffect触发dispatch导致组件重复渲染
问题根因
React 18 开发环境开启StrictMode时,会对首次挂载的组件自动执行一次「挂载→卸载→重新挂载」的检测流程,用来提前暴露副作用逻辑缺少清理、非幂等调用的问题。你遇到的双次渲染、PayPal按钮重复加载不是框架bug,是当前useEffect写法存在三个明显问题:
- 逻辑执行顺序错误:effect顶部无前置判断就直接异步调用
addPaypalScript,不管当前订单是否已支付、PayPal是否已加载,每次effect执行都会先插一次脚本 - 清理逻辑缺失:现有清理函数只做了
orderPayReset的dispatch,没有处理已插入的DOM脚本、未完成的异步请求,导致重挂载时副作用叠加 - 依赖数组不全:
order、success、orderId这些effect内用到的外部值全部没加入依赖,既会触发闭包陈旧问题,也会导致effect无意义重复执行
修复方案
按照下面的规则调整代码即可,不需要关闭StrictMode——这个检测逻辑仅在开发环境生效,生产构建后不会触发双执行,能帮你提前规避很多内存泄漏、状态不一致的问题:
- 给PayPal脚本加唯一ID标识,插入前先判断是否已存在,避免重复插入DOM
- 增加卸载标记,组件卸载后停止更新状态,避免内存泄漏
- 补全清理逻辑:组件卸载时取消未完成的请求、移除已插入的PayPal脚本、重置加载状态
- 补全useEffect依赖数组,调整执行顺序,删掉顶部无意义的立即执行函数
- SDK加载完成前不要渲染PayPal按钮,避免组件重复初始化
修正后的完整代码:
const dispatch = useDispatch(); const [sdkReady, setSdkReady] = useState(false); useEffect(() => { // 卸载标记,避免组件销毁后更新状态 let isUnmounted = false; // PayPal脚本全局唯一ID,防止重复插入 const PAYPAL_SCRIPT_ID = 'paypal-checkout-sdk'; // 异步请求取消器 const abortController = new AbortController(); const addPaypalScript = async () => { // 已存在脚本直接标记为就绪,不重复加载 const existScript = document.getElementById(PAYPAL_SCRIPT_ID); if (existScript) { if (window.paypal && !isUnmounted) setSdkReady(true); return; } try { const res = await axios.get('http://localhost:8000/api/config/paypal', { signal: abortController.signal }); const clientId = res.data; // 并发场景二次校验,避免重复插入 if (document.getElementById(PAYPAL_SCRIPT_ID)) return; const script = document.createElement('script'); script.id = PAYPAL_SCRIPT_ID; script.type = 'text/javascript'; script.src = `https://www.paypal.com/sdk/js?client-id=${clientId}`; script.async = true; script.onload = () => { if (!isUnmounted) setSdkReady(true); }; document.body.appendChild(script); } catch (err) { // 主动取消的请求不打印错误 if (err.name !== 'CanceledError') console.error('PayPal SDK加载失败', err); } }; // 按订单状态执行对应逻辑 if (!order || success) { dispatch(orderDetail(orderId)); } else if (!order.isPaid) { if (!window.paypal) { addPaypalScript(); } else { setSdkReady(true); } } // 完整清理逻辑 return () => { isUnmounted = true; abortController.abort(); dispatch(orderPayReset()); // 移除已插入的SDK脚本,保证重挂载时环境干净 const loadedScript = document.getElementById(PAYPAL_SCRIPT_ID); if (loadedScript) loadedScript.remove(); setSdkReady(false); }; }, [dispatch, order, success, orderId]); return ( sdkReady ? ( <PayPalButton amount={order.totalPrice} onSuccess={successPaymentHandler} /> ) : <div>支付组件加载中...</div> )
补充说明:如果使用的是官方react-paypal-js之类的封装库,这类库本身已经做了幂等处理,只需要按照文档传入参数即可,不需要手动插入script标签。
内容的提问来源于stack exchange,提问作者dwiki setyawan
相关产品推荐
相关产品推荐

