React 18严格模式下PayPal按钮重复问题的修复方法
Hey fellow dev! I’ve dealt with this exact React 18 Strict Mode + PayPal button duplication issue before, so I feel your pain. Let’s break down why this happens and how to fix it properly.
Why This Happens
React 18’s Strict Mode in development intentionally double-invokes useEffect (and other lifecycle hooks) to catch unintended side effects. Your current code adds a new PayPal script every time useEffect runs—so two runs mean two scripts loading, which leads to duplicate buttons.
Solution: Proper Initialization + Cleanup
The fix has two core parts:
- Track whether you’ve already added the PayPal script/initialized the button
- Clean up the script and button instance when the component unmounts (or before re-running
useEffect)
Here’s the updated code with these fixes:
import { useRef, useEffect } from 'react'; import axios from 'axios'; const YourCheckoutComponent = () => { const paypalButtonRef = useRef(null); const paypalScriptRef = useRef(null); const paypalButtonInstance = useRef(null); useEffect(() => { const addPayPalScript = async () => { // Skip if we already added the script if (paypalScriptRef.current) return; const { data: paypalClientId } = await axios('/api/config/paypal'); const script = document.createElement('script'); script.type = 'text/javascript'; script.src = `https://www.paypal.com/sdk/js?client-id=${paypalClientId}`; script.async = true; script.onload = () => { // Initialize PayPal button and save the instance for cleanup paypalButtonInstance.current = window.paypal.Buttons({ // Add your button config here: // createOrder: (data, actions) => { ... }, // onApprove: (data, actions) => { ... }, }).render(paypalButtonRef.current); }; paypalButtonRef.current.appendChild(script); paypalScriptRef.current = script; }; addPayPalScript(); // Cleanup function: undo everything we did return () => { // Remove the script from the DOM if (paypalScriptRef.current) { paypalScriptRef.current.remove(); paypalScriptRef.current = null; } // Destroy the PayPal button instance if (paypalButtonInstance.current) { paypalButtonInstance.current.close(); paypalButtonInstance.current = null; } }; }, []); // Empty dependency array ensures this runs once on mount return ( <li ref={paypalButtonRef} id="paypal-button" className="row center"></li> ); }; export default YourCheckoutComponent;
Key Details:
paypalScriptRef: Tracks if we’ve already injected the PayPal script, so we don’t add it twice even ifuseEffectruns twice.paypalButtonInstance: Saves the PayPal button instance so we can callclose()on it during cleanup—this fully destroys the button, preventing duplicates.- Cleanup Function: React runs this before re-running
useEffect(in Strict Mode development) and when the component unmounts. It removes the script and destroys the button instance, undoing all side effects.
Alternative: Check for Existing Script in DOM
If you prefer, you can also check if the PayPal script already exists in the document before adding it:
useEffect(() => { const addPayPalScript = async () => { // Check if PayPal SDK script is already present const existingScript = document.querySelector('script[src*="paypal.com/sdk/js"]'); if (existingScript) { // Re-render the button if script already exists window.paypal.Buttons({ // Your config here }).render(paypalButtonRef.current); return; } // If no script exists, create and add it (same as before) const { data: paypalClientId } = await axios('/api/config/paypal'); const script = document.createElement('script'); script.type = 'text/javascript'; script.src = `https://www.paypal.com/sdk/js?client-id=${paypalClientId}`; script.async = true; script.onload = () => { window.paypal.Buttons({ // Your config here }).render(paypalButtonRef.current); }; document.body.appendChild(script); }; addPayPalScript(); // Cleanup: Remove the button element return () => { const buttonElement = paypalButtonRef.current?.firstChild; if (buttonElement) buttonElement.remove(); }; }, []);
Final Notes
- This issue only happens in development with Strict Mode enabled—production builds won’t double-invoke
useEffect. But it’s still important to fix it to avoid confusion during development. - Always make sure your side effects (like adding scripts to the DOM) are idempotent or properly tracked/cleaned up to play nice with React’s Strict Mode.
内容的提问来源于stack exchange,提问作者noOne
相关产品推荐
相关产品推荐

