React中在success函数调用Apollo Client Mutation报错的修正方法
解决React Hook调用规则报错:在二维码扫描回调中调用Apollo Mutation
你在二维码扫描的success回调中调用Apollo Client的Mutation时触发了React Hook规则报错,原代码及错误信息如下:
原代码
import React, { useState, useEffect, useRef, useCallback } from "react"; import { Html5QrcodeScanner } from "html5-qrcode"; import beepSound from "./beep.mp3"; import emptySound from "./empty.mp3"; import invalidSound from "./invalid.mp3"; import { gql, useMutation } from "@apollo/client"; const USETICKET_MUTATION = gql` mutation useTicket($userId: Int!, $fundId: Int!) { useTicket(userId: $userId, fundId: $fundId) { ok error } } `; function Scanner() { const [scanResult, setScanResult] = useState(null); const [showScanSuccess, setShowScanSuccess] = useState(null); const scannerRef = useRef(null); const [useTicketMutation] = useMutation(USETICKET_MUTATION); const fundId = 1; useEffect(() => { var beepAudio = new Audio(beepSound); var invalidAudio = new Audio(invalidSound); var emptyAudio = new Audio(emptySound); const scanner = new Html5QrcodeScanner("reader", { qrbox: { width: 220, height: 220, }, fps: 5, verbose: false, }); scannerRef.current = scanner; scanner.render(success, error); // 5초 뒤 초기화 setTimeout(() => { const stopCameraButton = document.getElementById( "html5-qrcode-button-camera-stop" ); console.log(stopCameraButton); if (stopCameraButton) { stopCameraButton.remove(); } const readerDashboard = document.getElementById("reader__dashboard"); console.log(readerDashboard); if (readerDashboard) { readerDashboard.remove(); } const reader = document.getElementById("reader"); console.log(reader); if (reader) { reader.style.border = "none"; reader.style.borderRadius = "12px"; } }, 5000); function success(result) { scanner.pause(); result = JSON.parse(result); if (result.fundId !== fundId) { invalidAudio.play(); setShowScanSuccess("invalid"); setScanResult("올바른 큐알코드를 대주세요"); } else if (result.left <= 0) { emptyAudio.play(); setShowScanSuccess("empty"); setScanResult("티켓이 부족합니다"); } else { beepAudio.play(); setScanResult(result); setShowScanSuccess("accept"); useTicketMutation({ variables: { userId: result.userId, fundId: result.fundId, }, }); } setTimeout(() => { scanner.resume(); setShowScanSuccess(null); }, 1500); } function error(err) { console.warn(err); } return () => { if (scannerRef.current) { scannerRef.current.clear(); } }; }, []); return ( <div style={{ display: "flex", flexDirection: "column", width: "500px", height: "450px", borderRadius: 15, border: `${ showScanSuccess == "accept" ? "thick solid lime" : showScanSuccess === "empty" || showScanSuccess === "invalid" ? "thick solid red" : "medium solid navy" }`, backgroundColor: `${ showScanSuccess == "accept" ? "rgba(0,255,0,0.5)" : showScanSuccess === "empty" || showScanSuccess === "invalid" ? "rgba(255,0,0,0.5)" : "rgba(0,0,0,0.01)" }`, }} > <div id="reader" style={{ width: "100%", height: "100%", overflow: "hidden", }} /> <div style={{ width: "100%", display: "flex", justifyContent: "center", }} > {showScanSuccess == "accept" ? ( <h3>{scanResult.left}</h3> ) : showScanSuccess === "empty" || showScanSuccess === "invalid" ? ( <h4 style={{ color: "navy" }}>DBUS</h4> ) : ( <h4 style={{ color: "navy" }}>DBUS</h4> )} </div> </div> ); } export default Scanner;
错误信息
Line 88:9: React Hook "useTicketMutation" is called in function "success" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
修复方案
报错核心原因是:useTicketMutation作为Hook返回的函数,被放在useEffect内部的普通函数success中调用,同时useEffect依赖数组为空,既违反Hook规则,还会引发闭包导致的旧值引用问题。
步骤1:用useCallback包裹回调函数
将success和error函数提取到useEffect外部,用useCallback包裹并声明依赖项,确保函数引用稳定,符合Hook调用规则。
步骤2:补全useEffect依赖数组
将success、error等在useEffect中用到的外部变量加入依赖数组,避免闭包问题。
额外优化
用useRef存储音频实例,避免每次useEffect执行时重复创建;清理定时器防止组件卸载后内存泄漏。
修改后的完整代码
import React, { useState, useEffect, useRef, useCallback } from "react"; import { Html5QrcodeScanner } from "html5-qrcode"; import beepSound from "./beep.mp3"; import emptySound from "./empty.mp3"; import invalidSound from "./invalid.mp3"; import { gql, useMutation } from "@apollo/client"; const USETICKET_MUTATION = gql` mutation useTicket($userId: Int!, $fundId: Int!) { useTicket(userId: $userId, fundId: $fundId) { ok error } } `; function Scanner() { const [scanResult, setScanResult] = useState(null); const [showScanSuccess, setShowScanSuccess] = useState(null); const scannerRef = useRef(null); const beepAudioRef = useRef(null); const invalidAudioRef = useRef(null); const emptyAudioRef = useRef(null); const [useTicketMutation] = useMutation(USETICKET_MUTATION); const fundId = 1; // 用useCallback包裹success函数,明确依赖 const success = useCallback((result) => { const scanner = scannerRef.current; if (!scanner) return; scanner.pause(); result = JSON.parse(result); if (result.fundId !== fundId) { invalidAudioRef.current?.play(); setShowScanSuccess("invalid"); setScanResult("올바른 큐알코드를 대주세요"); } else if (result.left <= 0) { emptyAudioRef.current?.play(); setShowScanSuccess("empty"); setScanResult("티켓이 부족합니다"); } else { beepAudioRef.current?.play(); setScanResult(result); setShowScanSuccess("accept"); // 这里调用mutation不再违反规则 useTicketMutation({ variables: { userId: result.userId, fundId: result.fundId, }, }); } setTimeout(() => { scanner.resume(); setShowScanSuccess(null); }, 1500); }, [useTicketMutation, fundId]); const error = useCallback((err) => { console.warn(err); }, []); useEffect(() => { // 用ref存储音频实例,避免重复创建 beepAudioRef.current = new Audio(beepSound); invalidAudioRef.current = new Audio(invalidSound); emptyAudioRef.current = new Audio(emptySound); const scanner = new Html5QrcodeScanner("reader", { qrbox: { width: 220, height: 220, }, fps: 5, verbose: false, }); scannerRef.current = scanner; scanner.render(success, error); // 5초 뒤 초기화 const initTimer = setTimeout(() => { const stopCameraButton = document.getElementById( "html5-qrcode-button-camera-stop" ); if (stopCameraButton) { stopCameraButton.remove(); } const readerDashboard = document.getElementById("reader__dashboard"); if (readerDashboard) { readerDashboard.remove(); } const reader = document.getElementById("reader"); if (reader) { reader.style.border = "none"; reader.style.borderRadius = "12px"; } }, 5000); return () => { clearTimeout(initTimer); if (scannerRef.current) { scannerRef.current.clear(); } }; }, [success, error]); // 加入依赖数组 return ( <div style={{ display: "flex", flexDirection: "column", width: "500px", height: "450px", borderRadius: 15, border: `${ showScanSuccess == "accept" ? "thick solid lime" : showScanSuccess === "empty" || showScanSuccess === "invalid" ? "thick solid red" : "medium solid navy" }`, backgroundColor: `${ showScanSuccess == "accept" ? "rgba(0,255,0,0.5)" : showScanSuccess === "empty" || showScanSuccess === "invalid" ? "rgba(255,0,0,0.5)" : "rgba(0,0,0,0.01)" }`, }} > <div id="reader" style={{ width: "100%", height: "100%", overflow: "hidden", }} /> <div style={{ width: "100%", display: "flex", justifyContent: "center", }} > {showScanSuccess == "accept" ? ( <h3>{scanResult.left}</h3> ) : showScanSuccess === "empty" || showScanSuccess === "invalid" ? ( <h4 style={{ color: "navy" }}>DBUS</h4> ) : ( <h4 style={{ color: "navy" }}>DBUS</h4> )} </div> </div> ); } export default Scanner;
内容的提问来源于stack exchange,提问作者tworiver
相关产品推荐
相关产品推荐

