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

React跨组件传递ScryfallQuery函数报错的最佳实践咨询

问题根因

你碰到的报错是三个基础错误叠加导致的:

  • props传参与接收名称大小写不匹配:App.js里传入的prop是小驼峰scryfallQuery,FileHandler解构时写的是大驼峰ScryfallQuery,React对props名是精确大小写敏感匹配的,直接导致拿到的函数是undefined。
  • 函数组件中错误使用this:updateCards里写了this.ScryfallQuery(),函数组件没有类实例,this本身就是undefined,自然读不到属性。
  • ScryfallQuery函数本身逻辑错误:定义时没有声明接收query入参,内部直接调用JSON.stringify(query)会触发变量未定义错误;且sliceIntoBatches方法写在return语句之后,永远不会执行,属于无效代码。

除此之外你的代码还有两个隐藏的逻辑bug:一是在Array.forEach里写异步逻辑,会导致并发执行顺序失控、错误无法正常捕获;二是批量更新cards时直接取闭包里的cards值,会因为React state的异步特性,导致每次更新都拿到初始空值,最终只保留最后一批请求的卡牌数据。

即时修复步骤

1. 抽离纯逻辑为公共模块

API请求、通用工具方法这类和组件状态无关的代码,不要写在组件内部,单独建公共工具文件存放,比如新建src/utils/scryfall.js:

// 通用数组批量切分工具,全项目可复用
export function sliceIntoBatches(arr, batchSize) {
  const batchArr = []
  for (let i = 0; i < arr.length; i += batchSize) {
    batchArr.push(arr.slice(i, i + batchSize));
  }
  return batchArr;
}

// Scryfall API 请求方法
export async function ScryfallQuery(query) {
  const res = await fetch('https://api.scryfall.com/cards/collection', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(query)
  });
  if (!res.ok) throw new Error(`请求异常,状态码:${res.status}`);
  return res.json();
}

2. 修正FileHandler组件逻辑

首先删掉组件内部重复写的sliceIntoBatches、scryfallCollectionQuery冗余代码,直接从公共模块导入需要的方法,修正函数调用和state更新逻辑:

import { useState, useEffect } from 'react';
import { Card } from './Card.js';
// 直接导入公共方法即可,不需要通过props传
import { ScryfallQuery, sliceIntoBatches } from '../utils/scryfall';

export const FileHandler = ({
  cardInput, setCardInput, 
  cards, setCards, 
  previewCard, setPreviewCard
}) => {
  const reader = new FileReader();
  reader.addEventListener('load', function(){
    const allCards = this.result.split(/\r?\n/g);
    setCardInput(allCards)   
  });

  const parseInput = (e) => {
    e.preventDefault();
    setCardInput([]);
    reader.readAsText(document.getElementById('input').files[0])
  }

  const updateCards = async () => {
    const cardBatches = sliceIntoBatches(cardInput, 75);
    // 用for...of替代forEach处理异步,保证批处理顺序可控
    for (const cardBatch of cardBatches) {
      const queryArray = cardBatch
        .filter(cardName => cardName !== '')
        .map(cardName => ({name: cardName})); 
      try {
        const response = await ScryfallQuery({ identifiers: queryArray });
        if (response?.data?.length > 0) {
          // 用函数式更新state,避免闭包拿到旧的state值
          setCards(prevCards => [...prevCards, ...response.data]);
        } else if (response?.not_found?.length > 0) {
          console.error("未匹配到的卡牌: ", response.not_found);
        }
      } catch (err) {
        console.error("卡牌查询失败: ", err);
      }
    }
  }

  useEffect(() => {
    if (cardInput.length > 0 && cards.length === 0) {
      updateCards();
    }
  }, [cardInput]);

  return(
    <div className="App">
      <h1>Submit a .txt file of cards! </h1>
      <form target="_self" onSubmit={parseInput}>
          <input type="file" id="input" />
          <button id="submit">Submit</button>
          <pre id="preReader"></pre>
      </form>
      
     {cards &&
        <table id="table-head">
          <tbody>  
            <tr>
              <td id="card-number-title">#</td>
              <td id="card-name-title">Card Name</td>
              <td id="card-data-title">Colors</td>
              <td id="card-data-title">Rarity</td>
              <td id="card-data-title">ELO</td>
            </tr>
          </tbody>
        </table> } 
      {cards && cards.map(card => 
        <Card
          card={card} 
          setPreviewCard={setPreviewCard} />
        )} 
    </div>   
  )
}

export default FileHandler;

3. 清理App.js冗余传参

因为现在ScryfallQuery是直接在用到的组件里导入的,不需要再通过App组件逐层往下传,把App.js里相关的传参删掉即可:

// 删掉App.js里这行不需要的导入
// import {ScryfallQuery} from './Components/ScryfallQuery';

// 删掉FileHandler组件上的scryfallQuery传参
<FileHandler 
  cardInput={cardInput}
  setCardInput={setCardInput}
  previewCard={previewCard} 
  setPreviewCard={setPreviewCard}
  cards={cards}
  setCards={setCards}
/>
跨组件复用函数的最佳实践
  • 纯工具方法、API请求这类和React组件状态、生命周期完全无关的逻辑,直接抽成独立JS模块,哪里用哪里导入,不要通过props传递,这是成本最低、最易维护的方案。
  • 只有和组件内部状态绑定的逻辑(比如封装了当前组件state、setState方法的事件处理函数),才需要通过props向子组件传递。
  • 如果后续需要复用带全局状态的逻辑(比如请求时自动携带用户凭证、统一处理全局loading/报错、跨组件共享请求缓存),可以封装自定义Hook,或者用Context做依赖注入,不需要一开始就上复杂的全局状态方案。

内容的提问来源于stack exchange,提问作者dconnenc

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 21:57:27