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

React类组件转Hooks函数式组件实现方法

MinterCard类组件转Hooks函数组件实现

以下转换基于react-bootstrap、axios、Web3技术栈的NFT Mint通用场景,你可以直接对照原有类组件的业务字段调整参数。

类组件与Hooks核心映射规则

先明确对应关系,方便你理解底层逻辑:

  • 类组件this.state/this.setState 对应函数组件useState,建议按业务维度拆分多个独立状态,无需像类组件一样把所有字段塞进同一个state对象
  • 类组件生命周期componentDidMount/componentDidUpdate/componentWillUnmount 统一由useEffect承载,通过依赖数组控制触发时机,effect内部return的函数负责执行卸载时的清理逻辑
  • 类组件挂载在this上、不触发UI更新的实例变量,对应函数组件的useRef
  • 类组件自定义方法可直接在函数组件内声明,需要固定函数引用避免重复触发的场景用useCallback包裹

转换后完整组件代码

import { useState, useEffect, useCallback, useRef } from 'react'
import { Card, Button, Form, Alert, Spinner } from 'react-bootstrap'
import axios from 'axios'
import Web3 from 'web3'

// 替换为你项目实际的合约ABI、合约地址
const NFT_CONTRACT_ABI = []
const NFT_CONTRACT_ADDRESS = '0x替换为你的合约地址'
const MINT_PRICE = '0.01' // 单个NFT铸造价格,单位ETH
const MAX_MINT_AMOUNT = 5 // 单次最大铸造数量

export default function MinterCard() {
  // 拆分声明状态,每个状态独立维护
  const [web3Instance, setWeb3Instance] = useState(null)
  const [currentAccount, setCurrentAccount] = useState('')
  const [nftContract, setNftContract] = useState(null)
  const [mintCount, setMintCount] = useState(1)
  const [isMinting, setIsMinting] = useState(false)
  const [latestTxHash, setLatestTxHash] = useState('')
  const [error, setError] = useState('')
  const [mintedNftData, setMintedNftData] = useState(null)

  // 存储不需要触发重渲染的实例值
  const axiosCancelRef = useRef(null)

  // 钱包连接逻辑
  const connectWallet = useCallback(async () => {
    setError('')
    try {
      if (!window.ethereum) throw new Error('请先安装MetaMask钱包插件')
      const web3 = new Web3(window.ethereum)
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
      const contract = new web3.eth.Contract(NFT_CONTRACT_ABI, NFT_CONTRACT_ADDRESS)
      
      setWeb3Instance(web3)
      setCurrentAccount(accounts[0])
      setNftContract(contract)
    } catch (err) {
      setError(err.message || '钱包连接失败,请重试')
    }
  }, [])

  // NFT铸造核心逻辑
  const handleMint = useCallback(async () => {
    if (!nftContract || !currentAccount || !web3Instance) return
    setIsMinting(true)
    setError('')
    setLatestTxHash('')
    try {
      // 调用合约铸造方法,参数可根据你实际的合约逻辑调整
      const txReceipt = await nftContract.methods.mint(mintCount).send({
        from: currentAccount,
        value: web3Instance.utils.toWei(String(Number(MINT_PRICE) * mintCount), 'ether')
      })
      setLatestTxHash(txReceipt.transactionHash)
      
      // 铸造成功后拉取NFT元数据
      const newTokenId = txReceipt.events.Transfer.returnValues.tokenId
      const { data } = await axios.get(`/api/nft/metadata/${newTokenId}`, {
        cancelToken: axiosCancelRef.current.token
      })
      setMintedNftData(data)
    } catch (err) {
      if (!axios.isCancel(err)) {
        setError(err.message || '铸造失败,请检查网络或钱包余额')
      }
    } finally {
      setIsMinting(false)
    }
  }, [nftContract, currentAccount, web3Instance, mintCount])

  // 副作用:对应类组件挂载、更新、卸载生命周期
  useEffect(() => {
    // 初始化axios取消令牌
    axiosCancelRef.current = axios.CancelToken.source()
    // 页面加载时如果钱包已授权,自动连接
    if (window.ethereum?.selectedAddress) {
      connectWallet()
    }
    // 监听钱包账户切换事件
    const handleAccountSwitch = (accounts) => {
      setCurrentAccount(accounts[0] || '')
    }
    window.ethereum?.on('accountsChanged', handleAccountSwitch)

    // 清理逻辑:对应componentWillUnmount
    return () => {
      // 取消组件卸载时未完成的接口请求
      axiosCancelRef.current?.cancel('组件已卸载')
      // 移除事件监听,避免内存泄漏
      window.ethereum?.removeListener('accountsChanged', handleAccountSwitch)
    }
  }, [connectWallet])

  return (
    <Card style={{ width: '22rem', margin: '2rem auto' }}>
      <Card.Img variant="top" src={mintedNftData?.image || '/default-nft-cover.png'} />
      <Card.Body>
        <Card.Title>NFT 铸造通道</Card.Title>
        {error && <Alert variant="danger">{error}</Alert>}
        {latestTxHash && <Alert variant="success">铸造成功!交易哈希:{latestTxHash.slice(0, 10)}...{latestTxHash.slice(-8)}</Alert>}
        
        {!currentAccount ? (
          <Button variant="primary" className="w-100" onClick={connectWallet}>
            连接钱包开始铸造
          </Button>
        ) : (
          <>
            <Form.Group className="mb-3">
              <Form.Label>铸造数量(单次最多{MAX_MINT_AMOUNT}个)</Form.Label>
              <Form.Control
                type="number"
                min={1}
                max={MAX_MINT_AMOUNT}
                value={mintCount}
                onChange={(e) => setMintCount(Math.min(Number(e.target.value), MAX_MINT_AMOUNT))}
              />
            </Form.Group>
            <Button
              variant="success"
              className="w-100"
              disabled={isMinting}
              onClick={handleMint}
            >
              {isMinting ? (
                <><Spinner animation="border" size="sm" className="me-2" />铸造中...</>
              ) : (
                `铸造 ${mintCount} 个NFT(${(Number(MINT_PRICE)*mintCount).toFixed(2)} ETH)`
              )}
            </Button>
            <div className="mt-2 text-muted small text-end">
              连接地址:{currentAccount.slice(0,6)}...{currentAccount.slice(-4)}
            </div>
          </>
        )}
      </Card.Body>
    </Card>
  )
}

关键逻辑说明

  • 状态管理差异:和类组件全量state合并更新不同,useState是单状态独立替换更新,拆分后状态依赖更清晰,不会出现无关字段更新触发不必要逻辑的问题
  • 生命周期执行逻辑:useEffect的执行时机是浏览器完成页面绘制之后,不会阻塞渲染;依赖数组为空时仅在组件挂载后执行一次,依赖数组传入变量时,变量引用变化就会重新执行effect
  • 内存泄漏规避:所有事件监听、未完成的异步请求,都必须在effect的return清理函数中移除/取消,这部分和类组件在componentWillUnmount写清理逻辑的目标一致
  • 性能注意点:传递给子组件的回调函数、作为effect依赖的函数,必须用useCallback包裹,否则每次组件重渲染都会生成新的函数引用,导致effect重复执行、子组件无效重渲染

如果你原有类组件包含其他业务逻辑,按照上述映射规则逐块迁移即可,不要刻意模仿类组件的写法把所有状态塞进一个useState,拆分状态是Hooks写法下更易维护的实践。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 15:09:21