PAYATHON 2026

JS 在 console 中有效,但在 React App 中无法创建表单

支付小周

结论

问题通常不在于 React 能不能操作 DOM,而是 console、普通 HTML 与 React 的执行环境不同:

  1. React 中声明的 var iyziInit 处于模块或组件作用域,不会自动成为 window.iyziInit。如果第三方 bundle.js 从全局对象读取配置,就找不到这个变量。
  2. 如果代码在组件渲染期间执行,外部脚本开始加载时,#iyzipay-checkout-form 可能还没有挂载到 DOM。

解决办法是把相关代码放进组件挂载后的 useEffect:先将配置显式赋给 window.iyziInit,确认目标容器已经存在,再加载第三方脚本。

为什么在 console 和普通 HTML 中有效

在浏览器 console 或传统的非模块 <script> 中声明:

var iyziInit = {};

第三方脚本通常可以把它当作全局变量访问,其效果接近:

window.iyziInit = {};

React 项目通常会经过 webpack、Vite 等工具打包,并以 ES Module 方式运行。组件文件中的变量会被限制在模块作用域内:

var iyziInit = {};

React 代码可以访问这个 iyziInit,但之后加载的远程 bundle.js 不一定能访问。

组件函数开始执行,也不代表对应元素已经写入真实 DOM。如果直接在组件函数中加载脚本,执行顺序可能是:

执行组件函数
→ 添加第三方 script
→ 第三方脚本开始运行
→ React 才把支付表单容器挂载到 DOM

而在 console 中执行代码时,页面通常已经渲染完成,目标 div 也已经存在。

React 中的正确加载方式

可以把支付配置和脚本加载逻辑放进 useEffect:

import { useEffect } from "react";

export default function CheckoutForm() {
  useEffect(() => {
    const container = document.getElementById("iyzipay-checkout-form");

    if (!container) {
      console.error("iyzipay-checkout-form container was not found");
      return;
    }

    window.iyziInit = {
      currency: "TRY",
      token: "23bfa27a-172a-493d-b42f-8b091589eaa0",
      price: 2.4,
      locale: "tr",
      baseUrl: "https://sandbox-api.iyzipay.com",
      merchantGatewayBaseUrl:
        "https://sandbox-merchantgw.iyzipay.com",
      registerCardEnabled: true,
      bkmEnabled: true,
      bankTransferEnabled: false,
      bankTransferRedirectUrl: "https://localhost:3000/callback",
      bankTransferCustomUIProps: {},
      campaignEnabled: false,
      creditCardEnabled: true,
      bankTransferAccounts: [],
      userCards: [],
      fundEnabled: true,
      memberCheckoutOtpData: {},
      force3Ds: false,
      isSandbox: true,
      storeNewCardEnabled: true,
      paymentWithNewCardEnabled: true,
      enabledApmTypes: ["SOFORT", "IDEAL", "QIWI", "GIROPAY"],
      payWithIyzicoUsed: false,
      payWithIyzicoEnabled: true,
      payWithIyzicoCustomUI: {},
      buyerName: "John",
      buyerSurname: "Doe",
      merchantInfo: "",
      cancelUrl: "",
      buyerProtectionEnabled: false,
      hide3DS: false,
      gsmNumber: "",
      email: "email@email.com",
      checkConsumerDetail: {},
      subscriptionPaymentEnabled: false,
      ucsEnabled: false,
      fingerprintEnabled: false,
      payWithIyzicoFirstTab: false,
      metadata: {}
    };

    const script = document.createElement("script");
    script.src =
      "https://sandbox-static.iyzipay.com/checkoutform/v2/bundle.js?v=1634688058781";
    script.async = true;
    script.dataset.iyzicoCheckout = "true";

    script.onerror = () => {
      console.error("Failed to load iyzico checkout script");
    };

    document.head.appendChild(script);

    return () => {
      script.remove();
      container.replaceChildren();

      if (window.iyziInit) {
        delete window.iyziInit;
      }
    };
  }, []);

  return <div id="iyzipay-checkout-form" className="responsive" />;
}

其中:

window.iyziInit = { ... };

把初始化配置放到全局对象上,远程脚本就能读取它。

useEffect(() => {
  // ...
}, []);

让代码在组件挂载到真实 DOM 后运行。

<div id="iyzipay-checkout-form" />

为第三方脚本提供指定 ID 的容器。

如果 API 返回的是完整脚本文本

不要通过 JSX 或 dangerouslySetInnerHTML 直接插入 <script>:

<div dangerouslySetInnerHTML={{ __html: scriptText }} />

浏览器通常不会执行通过 innerHTML 插入的 <script>。应先解析初始化数据并赋值给 window.iyziInit,再通过 document.createElement("script") 加载外部资源。

如果后端只能返回完整的 JavaScript 字符串,最好调整接口,改为返回结构化配置,例如:

{
  "currency": "TRY",
  "token": "...",
  "price": 2.4,
  "locale": "tr"
}

前端拿到配置后再执行:

window.iyziInit = checkoutConfig;

不建议使用 eval() 执行接口返回的脚本文本。这样做有明显的安全风险,错误处理和生命周期管理也会更麻烦。

React Strict Mode 的影响

在 React 18 开发环境中,StrictMode 可能会执行一次”挂载、清理、再次挂载”,以检查副作用是否安全。这可能让第三方脚本加载两次。

加载前可以先检查并移除已有脚本:

const oldScript = document.querySelector(
  'script[data-iyzico-checkout="true"]'
);

if (oldScript) {
  oldScript.remove();
}

但删除 <script> 未必能清除第三方脚本已经注册的事件监听器或全局状态。如果支付 SDK 提供销毁方法,应优先调用官方销毁 API。暂时关闭 StrictMode 可以帮助定位问题,但不能作为正式修复方案。

其他注意事项

支付表单最好使用一个空容器,让 React 不去管理它的内部子节点:

<div id="iyzipay-checkout-form" />

不要同时让 React 向这个 div 渲染其他内容。否则,组件重新渲染或重新挂载时,第三方脚本插入的节点可能会被清除。

还应检查浏览器 console 和 Network 面板,排查以下情况:

  • iyziInit is not defined
  • 第三方脚本被 Content Security Policy 拦截
  • 脚本请求失败
  • 支付 token 已过期或已经使用
  • 页面中存在重复的 iyzipay-checkout-form ID
  • 路由切换导致组件卸载后重新挂载
  • 广告拦截器或隐私扩展阻止支付脚本

生产环境中,不要把支付凭据或可复用的敏感配置硬编码在前端。支付初始化信息和 token 应由服务端按照支付网关的要求生成,再交给前端渲染表单。

备注:内容仅供参考。