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

Shopify结账配送选项扩展中useSubscription引发无限循环问题求助

Shopify结账配送选项扩展中useSubscription引发无限循环问题求助

看起来你遇到了两个关联的问题:一个是useSubscription触发的无限循环,另一个是初始渲染时多次打印test的问题。我们一步步来拆解原因并给出修复方案:

一、无限循环的原因分析

你的代码里存在一个闭环逻辑:

  1. 你订阅了deliveryGroups,并把它作为useEffect的依赖
  2. 在这个useEffect里调用updateAttributes,而applyAttributeChange会修改结账的自定义属性
  3. 自定义属性的更新会触发Shopify checkout状态变化,进而导致deliveryGroups重新订阅更新
  4. 这又会再次触发useEffect,形成无限循环

二、无限循环的修复方案

我们需要打破这个闭环,调整触发更新的逻辑,并避免无意义的状态修改:

1. 正确使用useSubscription获取稳定值

Shopify UI扩展返回的lines、isTargetSelected等都是订阅对象,直接访问.current可能会拿到过时的引用,应该用useSubscription获取稳定的当前值:

// 替换直接访问.current的方式,改用useSubscription获取订阅值
const currentLines = useSubscription(lines);
const currentIsTargetSelected = useSubscription(isTargetSelected);
const currentShippingAddress = useSubscription(shippingAddress);
const currentTarget = useSubscription(target);

2. 调整updateAttributes的触发条件

不要依赖deliveryGroups来触发属性更新,而是仅在选中状态、dalMin或dalMax变化时执行:

useEffect(() => {
  if (currentIsTargetSelected && dalMin && dalMax) {
    updateAttributes();
  }
}, [currentIsTargetSelected, dalMin, dalMax]); // 仅依赖必要的状态

3. 避免无意义的属性更新

在updateAttributes中先检查当前属性值是否已经和目标值一致,只有不同时才调用applyAttributeChange:

const updateAttributes = async () => {
  try {
    // 先查询当前的自定义属性值
    const attributeRes = await query(`{
      checkout {
        customAttributes {
          key
          value
        }
      }
    }`);
    const currentAttributes = attributeRes.data.checkout.customAttributes.reduce((acc, attr) => {
      acc[attr.key] = attr.value;
      return acc;
    }, {});

    const newDalMin = dalMin.toISOString().slice(0, 10);
    const newDalMax = dalMax.toISOString().slice(0, 10);

    // 仅当值不同时才执行更新
    if (currentAttributes.dal_min !== newDalMin) {
      await applyAttributeChange({
        type: 'updateAttribute',
        key: 'dal_min',
        value: newDalMin,
      });
    }

    if (currentAttributes.dal_max !== newDalMax) {
      await applyAttributeChange({
        type: 'updateAttribute',
        key: 'dal_max',
        value: newDalMax,
      });
    }
  } catch (err) {
    console.error('更新自定义属性失败:', err);
  }
};

三、初始渲染多次打印test的原因

你的扩展挂载在purchase.checkout.shipping-option-item.render-after,每个配送选项都会生成一个该组件的实例(你有4个配送方法,就有4个组件实例)。而React组件每次状态更新时都会重新渲染,所以console.log('test')会在每个实例的每次渲染时打印,这是正常现象。

如果想减少调试打印,可以把console.log移到特定的useEffect里,或者只在开发环境打印:

useEffect(() => {
  if (process.env.NODE_ENV === 'development') {
    console.log('组件渲染:', shippingMethodTitle);
  }
}, [shippingMethodTitle]);

完整修改后的代码示例

import {
  Text,
  useApi,
  useTranslate,
  reactExtension,
  useSubscription,
} from '@shopify/ui-extensions-react/checkout';
import { useEffect, useState } from 'react';
import i18nIsoCountries from 'i18n-iso-countries';

i18nIsoCountries.registerLocale(require("i18n-iso-countries/langs/en.json"));

export default reactExtension(
  'purchase.checkout.shipping-option-item.render-after',
  () => <Extension />,
);

function Extension() {
  const { 
    shop, 
    shippingAddress, 
    lines, 
    target, 
    query, 
    i18n, 
    applyAttributeChange, 
    isTargetSelected, 
    deliveryGroups 
  } = useApi();
  const translate = useTranslate();
  const [items, setItems] = useState([]);
  const [dalMin, setDalMin] = useState();
  const [dalMax, setDalMax] = useState();

  // 使用useSubscription获取稳定的订阅值
  const currentLines = useSubscription(lines);
  const currentIsTargetSelected = useSubscription(isTargetSelected);
  const currentShippingAddress = useSubscription(shippingAddress);
  const currentTarget = useSubscription(target);

  console.log('test')

  const country = i18nIsoCountries.getName(currentShippingAddress.countryCode, 'en');
  const shopDomain = shop.myshopifyDomain;
  const shippingMethodTitle = currentTarget.title;

  // 仅在选中状态、dalMin/dalMax变化时更新属性
  useEffect(() => {
    if (currentIsTargetSelected && dalMin && dalMax) {
      updateAttributes();
    }
  }, [currentIsTargetSelected, dalMin, dalMax]);

  const updateAttributes = async () => {
    try {
      // 查询当前自定义属性值
      const attributeRes = await query(`{
        checkout {
          customAttributes {
            key
            value
          }
        }
      }`);
      const currentAttributes = attributeRes.data.checkout.customAttributes.reduce((acc, attr) => {
        acc[attr.key] = attr.value;
        return acc;
      }, {});

      const newDalMin = dalMin.toISOString().slice(0, 10);
      const newDalMax = dalMax.toISOString().slice(0, 10);

      // 避免重复更新
      if (currentAttributes.dal_min !== newDalMin) {
        await applyAttributeChange({
          type: 'updateAttribute',
          key: 'dal_min',
          value: newDalMin,
        });
      }

      if (currentAttributes.dal_max !== newDalMax) {
        await applyAttributeChange({
          type: 'updateAttribute',
          key: 'dal_max',
          value: newDalMax,
        });
      }
    } catch (err) {
      console.error('更新属性失败:', err);
    }
  };

  useEffect(() => {
    async function fetchSkus() {
      try {
        const res = await query(`{
          nodes(ids:[${currentLines.map((l) => `"${l.merchandise.id}"`).join(',')}]){
            ...on ProductVariant{
              sku
            }
          }
        }`);
        if (res.errors) {
          const err = new Error('获取SKU失败');
          err.errors = res.errors;
          throw err;
        }
        setItems(res.data.nodes);
      } catch (err) {
        console.error(err)
      }
    }

    async function fetchDal() {
      try {
        await fetchSkus();
        // 补充你的API请求逻辑
        const res = await fetch(`https://你的API地址`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            skus: items.map(item => item.sku),
            country: country
          })
        });
        const json = await res.json();
        setDalMin(new Date(json.dals[0].dalMin));
        setDalMax(new Date(json.dals[0].dalMax));
      } catch (err) {
        console.error(err)
      }
    }

    fetchDal();
  }, [currentLines, country]); // 依赖变化时重新获取数据

  if (dalMin && dalMax) {
    return (
      <Text size='small' appearance='subdued'>
        {translate('delai', { 
          dalMin: i18n.formatDate(dalMin, { dateStyle: 'short' }), 
          dalMax: i18n.formatDate(dalMax, { dateStyle: 'short' }) 
        })}
      </Text>
    );
  }

  return null; // 数据未加载时返回null,避免空渲染
}

备注:内容来源于stack exchange,提问作者Kenyuki

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.20 10:28:13