当 solidity 事件被触发时,我的 React 状态变量在回调中为空

My React state variables are empty in callback when a solidity event gets fired

我正在使用 reacttruffle/ganache。我在我的智能合约中定义了一些事件,这些事件会在某些事情发生时触发。我有一个专门用于处理合同实例及其事件的反应组件。这是显示我遇到的问题的精简版...

import React, {useState, useEffect} from "react";
import Client from '../build/contracts/Client.json'

const ClientBugDemo = (props) => {

    /**
     * props contains... 
     * contractAddress
     * web3
     */

    const [contractAddress, setContractAddress] = useState(0);
    useEffect(()=>{},[contractAddress]);

    const [contractInstance, setContractInstance] = useState(0);
    useEffect(()=>{},[contractInstance]);

    const [name, setName] = useState("");
    useEffect(()=>{},[name]);

    function _getContractInstance(address, web3){
        var instance = new web3.eth.Contract(
            Client.abi,
            address,
        );
        return instance;
    }

    if (contractAddress === 0){
        if (props.contractAddress){
            setContractAddress(props.contractAddress);

            var instance = _getContractInstance(props.contractAddress, props.web3);
            setContractInstance(instance);

            instance.events.MyCustomEvent(ev_MyCustomEventHandler);

            instance.methods.getName()
            .call()
            .then((_name, err)=>{
                if (err){
                    console.log("error : ", err);
                }
                else {
                    setName(_name);
                }
            });
    
        }
    }

    function ev_MyCustomEventHandler(error, event){
        console.log("Why is my contract Instance empty? ", contractInstance);
        console.log("But my props are fine? ", props.contractAddress);
    }
    
    return (
        <div>
            <div>Contract Address = {contractAddress}</div>
            <div>Name = {name}</div>
            <div>Have Contract = {contractInstance?"Yes":"No"}</div>
        </div>
    )
}

export default ClientBugDemo;

我为我的客户合约创建了上述组件之一,并为其提供了一个有效的 contractAddressweb3 对象。

它成功加载并显示了合约地址、名称,我可以看到合约实例是有效的。一切都很好,我有一个工作组件。

Contract Address = 0x64cCa2C98c13bB06e7D5b35Ae3af03B4A4C4ec71
Name = Taylor
Have Contract = Yes

然后自定义事件将触发,处理程序 ev_MyCustomEventHandler 将按预期被调用。

问题是在该函数调用期间我的本地 contractAddress 值为零。事实上,据我所知,我在 React 中的所有局部状态变量都处于空状态,就好像组件正在重新渲染一样。然而 props 有其原始值。

所以我的控制台日志显示

Why is my contract Instance empty? 0
But my props are fine? 0x64cCa2C98c13bB06e7D5b35Ae3af03B4A4C4ec71

我尝试了无数种方法,包括重构组件,认为在分配事件处理程序时状态可能会以某种方式持续存在。我真的无法让它工作。

好吧,在 React Hooks 中对闭包等的大量研究使我找到了这个解决方案...

import React, {useState, useEffect} from "react";
import Client from '../build/contracts/Client.json'

const ClientBugDemo = (props) => {

    /**
     * props contains... 
     * contractAddress
     * web3
     */

    const [contractAddress, setContractAddress] = useState(0);
    useEffect(()=>{},[contractAddress]);

    const [contractInstance, setContractInstance] = useState(0);
    useEffect(()=>{},[contractInstance]);

    const [name, setName] = useState("");
    useEffect(()=>{},[name]);

    const [semaphore, setSemaphore] = useState(0);
    useEffect(()=>{

        console.log("Now my Contract Instance is not empty ", contractInstance);
        if (sempahore !== 0){
            // I can do what I need to do in response to the event being called
            contractInstance.methods.doSomethingFunky()
            .call()
            .then((_funky, err)=>{
                console.log("got something funky ", _funky);
            })

            // reset the state of the "mutex" so we don't come in here endlessly
            setSemaphore(0);
        }

    },[semaphore, contractInstance]);


    function _getContractInstance(address, web3){
        var instance = new web3.eth.Contract(
            Client.abi,
            address,
        );
        return instance;
    }

    if (contractAddress === 0){
        if (props.contractAddress){
            setContractAddress(props.contractAddress);
            var instance = _getContractInstance(props.contractAddress, props.web3);
            setContractInstance(instance);
            instance.events.MyCustomEvent(ev_MyCustomEventHandler);

            instance.methods.getName()
            .call()
            .then((_name, err)=>{
                if (err){
                    console.log("error : ", err);
                }
                else {
                    setName(_name);
                }
            });
    
        }
    }

    function ev_MyCustomEventHandler(error, event){
        if (!error) setSemaphore(event);
    }
    
    return (
        <div>
            <div>Contract Address = {contractAddress}</div>
            <div>Name = {name}</div>
            <div>Have Contract = {contractInstance?"Yes":"No"}</div>
        </div>
    )
}

export default ClientBugDemo;

认为 正在发生的事情是,我收到的响应 solidity 事件的回调是 运行 作为一个独立的代码片段。 React 不知道它有任何依赖关系。

我的解决方案是添加一种 semaphore 变量,可以在事件到达时对其进行监视。当回调触发时,我将 semaphore 的值设置为包含事件值,然后推迟回 React Hooks。

如果我使用 useEffectsemaphore 包含在 useEffect 定义中,从而使 semaphore 依赖于我的 contractInstance,那么在useEffect 可以访问状态变量并且它有效。

我将继续寻找更优雅的解决方案,因为我承认这感觉像 work-round,但现在这是一个我可以重复的模式,而无需大量重复代码或绝对我所有的代码都嵌入了嵌套的 useEffect 匿名函数中。