You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React 18严格模式下PayPal按钮重复问题的修复方法

Fixing Duplicate PayPal Buttons in React 18 Strict Mode

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:

  1. Track whether you’ve already added the PayPal script/initialized the button
  2. 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 if useEffect runs twice.
  • paypalButtonInstance: Saves the PayPal button instance so we can call close() 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 19:42:36