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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 11:37:05