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

如何修复CryptoExchange组件的React Hooks调用顺序错误?

问题

我开发了如下的CryptoExchange组件:

import { useEffect, useState } from "react";
import { Col, Row, Space, Select } from "antd";
import styles from "./CryptoExchange.module.css";

import bitcoin from "../../../assets/Bitcoin-3.svg";
import Image from "next/image";
import { DUMMY_TOKENS } from "@/src/api/api";
import { ICrypto } from "@/src/models/tokens";
import useTranslation from "next-translate/useTranslation";
import { useQuery } from "@tanstack/react-query";
import numeral from "numeral";
import Api from "@/src/api/real-api";

const numFormat = "0,0.00";
const crypto = "crypto";

const CryptoExchange = () => {
  const [selectedToken, setSelectedToken] = useState<ICrypto>(DUMMY_TOKENS[0]);
  const [currency, setCurrency] = useState<string>("USD");
  const [price, setPrice] = useState(0);
  const [high_24h, setHigh_24h] = useState(0);
  const [low_24h, setLow_24h] = useState(0);
  const [volume_24h, setVolume_24h] = useState(0);
  const [price_24h, setPrice_24h] = useState(0);

  const { data, isError, isLoading, error } = useQuery(
    [crypto],
    Api.fetchTokens
  );

  useEffect(() => {
    if (!isLoading && !isError && data) {
      exchangeHandler(currency, selectedToken);
    }
  }, [currency, selectedToken, data, isError, isLoading]);

  const exchangeHandler = (currency: string, token: ICrypto) => {
    if (currency === "USD") {
      setPrice(+token.tokenStat.price * 1);
      setPrice_24h(+token.tokenStat.price_24h * 1);
      setHigh_24h(+token.tokenStat.high_24h * 1);
      setLow_24h(+token.tokenStat.low_24h * 1);
      setVolume_24h(+token.tokenStat.volume_24h * 1);
    } else if (currency === "EUR") {
      setPrice(+token.tokenStat.price * 0.92);
      setPrice_24h(+token.tokenStat.price_24h * 0.92);
      setHigh_24h(+token.tokenStat.high_24h * 0.92);
      setLow_24h(+token.tokenStat.low_24h * 0.92);
      setVolume_24h(+token.tokenStat.volume_24h * 0.92);
    } else if (currency === "RSD") {
      setPrice(+token.tokenStat.price * 107.66);
      setPrice_24h(+token.tokenStat.price_24h * 107.66);
      setHigh_24h(+token.tokenStat.high_24h * 107.66);
      setLow_24h(+token.tokenStat.low_24h * 107.66);
      setVolume_24h(+token.tokenStat.volume_24h * 107.66);
    }
  };

  if (isLoading) return <h2>Loading...</h2>;
  if (isError)
    return (
      <>
        <h2>Something went wrong...</h2>
        <p>{error.toString()}</p>
      </>
    );

  const { t } = useTranslation("common");

  const tokensSelectOptions = data.map((el) => {
    return (
      <Select.Option key={el.id} value={JSON.stringify(el)} label={el.name}>
        {el.name}
      </Select.Option>
    );
  });

  const currencySelectOptions = [
    <Select.Option
      value="USD"
      label={`${selectedToken.symbol}/USD`}
      key={`${selectedToken.symbol}/USD`}
    >
      {`${selectedToken.symbol}/USD`}
    </Select.Option>,
    <Select.Option
      value="EUR"
      label={`${selectedToken.symbol}/EUR`}
      key={`${selectedToken.symbol}/EUR`}
    >
      {`${selectedToken.symbol}/EUR`}
    </Select.Option>,
    <Select.Option
      value="RSD"
      label={`${selectedToken.symbol}/RSD`}
      key={`${selectedToken.symbol}/RSD`}
    >
      {`${selectedToken.symbol}/RSD`}
    </Select.Option>,
  ];

  const handleSelectedToken = (value: any) => {
    const token = value && JSON.parse(value.value);
    setSelectedToken(token);
  };

  return (
    <div>
      <Row className={styles.gutterRow}>
        <Col md={19} sm={19} xs={15} className={styles.gutterCol}>
          <div className={styles.select}>
          <Image src={bitcoin} alt="bitcoin" className={styles.image} />
          <Space wrap>
            <Select
              defaultValue={{
                value: data[0].name,
                label: data[0].name,
              }}
              labelInValue={true}
              style={{ width: 100 }}
              onChange={(value) => handleSelectedToken(value)}
              >
              {tokensSelectOptions}
            </Select>
              
            <Select
              style={{ width: 100 }}
              onChange={(value) => setCurrency(value.value)}
              defaultValue={{
                value: "USD",
                label: `${selectedToken.symbol}/USD`,
              }}
              labelInValue={true}
            >
              {currencySelectOptions}
            </Select>
          </Space>
          </div>
          <div>
            <p className={styles.orangeP}>
              {numeral(price).format(numFormat)} {currency}
            </p>
          </div>
          <div>
            <div className={styles.changes}>
              <p className={styles.orangeP}>
                {numeral(price_24h).format(numFormat)}
              </p>
              <p className={styles.orangeP}>
                {numeral(selectedToken.tokenStat.change_24h).format(numFormat)}
              </p>
            </div>
            <h6>24h {t("exchange.changes")}</h6>
          </div>
          <div>
            <p>{numeral(high_24h).format(numFormat)}</p>
            <h6>24h {t("exchange.high")}</h6>
          </div>
          <div>
            <p>{numeral(low_24h).format(numFormat)}</p>
            <h6>24h {t("exchange.low")} </h6>
          </div>
          <div>
            <p>{numeral(volume_24h).format(numFormat)}</p>
            <h6>
              24h {t("exchange.volume")}({selectedToken.symbol})
            </h6>
          </div>
        </Col>
        <Col md={1} sm={1} xs={2} offset={1}>
          <button className={styles.btn}>{t("exchange.buy")}</button>
        </Col>
        <Col  md={2} sm={2} xs={5} offset={1}>
          <button className={styles.btn}>{t("exchange.add")}</button>
        </Col>
      </Row>
    </div>
  );
};

export default CryptoExchange;

运行时控制台出现警告:

Warning: React has detected a change in the order of Hooks called by CryptoExchange. This will lead to bugs and errors if not fixed.

我尝试调整Hooks调用顺序但未解决问题,请问该如何修复这个错误?

解决方案

这个警告的核心原因是你在组件渲染流程中提前返回JSX,导致后续的Hooks在某些渲染周期中没有被调用,违反了React Hooks的核心规则:Hooks必须在每次渲染中按完全相同的顺序执行。

具体问题点

你在useTranslation Hook调用之前,就加入了两个条件返回语句:

if (isLoading) return <h2>Loading...</h2>;
if (isError)
  return (
    <>
      <h2>Something went wrong...</h2>
      <p>{error.toString()}</p>
    </>
  );

当isLoading或isError为true时,组件会直接返回,useTranslation不会被执行;而当加载完成后,这个Hook又会被正常调用。前后两次渲染的Hooks调用顺序不一致,直接触发了警告。

修复步骤

把所有Hooks调用提前到所有条件返回语句之前,确保每次渲染都按相同顺序执行所有Hooks:

修改后的核心代码片段:

const CryptoExchange = () => {
  // 所有Hooks调用都放在组件最顶部
  const [selectedToken, setSelectedToken] = useState<ICrypto>(DUMMY_TOKENS[0]);
  const [currency, setCurrency] = useState<string>("USD");
  const [price, setPrice] = useState(0);
  const [high_24h, setHigh_24h] = useState(0);
  const [low_24h, setLow_24h] = useState(0);
  const [volume_24h, setVolume_24h] = useState(0);
  const [price_24h, setPrice_24h] = useState(0);

  const { data, isError, isLoading, error } = useQuery(
    [crypto],
    Api.fetchTokens
  );

  // 提前调用useTranslation,放在条件判断之前
  const { t } = useTranslation("common");

  useEffect(() => {
    if (!isLoading && !isError && data) {
      exchangeHandler(currency, selectedToken);
    }
  }, [currency, selectedToken, data, isError, isLoading]);

  // exchangeHandler定义...

  // 条件判断放在所有Hooks之后
  if (isLoading) return <h2>Loading...</h2>;
  if (isError)
    return (
      <>
        <h2>Something went wrong...</h2>
        <p>{error.toString()}</p>
      </>
    );

  // 剩余组件逻辑和JSX返回...
};

额外优化建议

你目前在exchangeHandler中硬编码了汇率,建议把汇率数据抽成常量对象,让代码更易维护:

const EXCHANGE_RATES = {
  USD: 1,
  EUR: 0.92,
  RSD: 107.66
};

const exchangeHandler = (currency: string, token: ICrypto) => {
  const rate = EXCHANGE_RATES[currency as keyof typeof EXCHANGE_RATES] || 1;
  setPrice(+token.tokenStat.price * rate);
  setPrice_24h(+token.tokenStat.price_24h * rate);
  setHigh_24h(+token.tokenStat.high_24h * rate);
  setLow_24h(+token.tokenStat.low_24h * rate);
  setVolume_24h(+token.tokenStat.volume_24h * rate);
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 01:39:55