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
相关产品推荐
相关产品推荐

