【问题标题】:Update state within listener that is inside useEffect更新 useEffect 内的侦听器内的状态
【发布时间】:2021-05-03 14:15:17
【问题描述】:

我有一个名为 useQueryEvents 的钩子,它 1) 为用户获取所有过去的交易,以及 2) 监听网络的传入/传出交易。在这两种情况下,事务都被传递到函数 addActionToActivity,该函数只是将其附加到活动数组并在键 activity 下的上下文状态中更新它。

我无法正确同步活动。每当状态更新时,它都没有最后一笔交易,因为它总是落后一步。如果我将activity 添加到依赖关系中,它会起作用,但随后会启动一个新的侦听器(由于使用新的activity 值再次调用整个函数),这会导致无限循环,不断切换状态。

function useQueryEvents() {
  const { state: { connectedNetwork, selectedWallet, activity },
  } = useContext(LocalContext);

  useEffect(() => {
    async function bootstrapQueryEvents() {
      // First get all the past transactions
      const transactions = await getAllPastTransactions();
      const contract = await getContract();

      // Now save them to context state via addActionToActivity
      await addActionToActivity(transactions, activity);

      // Now that all the past transactions have been saved
      // listen to all incoming/outgoing transactions and
      // save to context state via addActionToActivity
      contract.on('Transfer', async (from, to, amount, event) => {
        console.log(`${from} sent ${ethers.utils.formatEther(amount)} to ${to}`);
        const transaction = await formatEventToTransaction(event);
        await addActionToActivity(transaction, activity);
      });
    }

    bootstrapQueryEvents();
  }, [selectedAsset, connectedNetwork, selectedWallet]); // <- I've tried adding `activity` here
}

有什么想法可以在访问侦听器内更新的activity 值的同时更新状态启动侦听器的新实例?或者也许我可以完全采取不同的方法?

提前致谢

【问题讨论】:

  • 您能把addActionToActivity中使用的代码贴出来吗?只是想检查它如何处理事务数组和事务。另外,当您的意思是它减一时,这是否意味着 contract.on 确实会触发重新渲染?
  • 如果不了解 addActionToActivity 中的内容,我们将无法真正帮助您。你如何更新状态?直接修改状态吗?
  • useEffect 中为什么没有使用任何依赖项? selectedAsset 来自哪里?

标签: javascript reactjs react-native listener


【解决方案1】:

您可以通过将逻辑拆分为两个 useEffects 来解决此问题。现在你做两件事:

  1. 您获取交易
  2. 设置事件监听器

问题是如果不同时做这两件事,你就不能再次运行这个钩子。正如您所说,活动对象不是您想要的,因为活动对象是在设置事件侦听器时传递的。

将它分成两个钩子可能看起来像这样:

function useQueryEvents() {
  const { state: { connectedNetwork, selectedWallet, activity },
  } = useContext(LocalContext);
  const [contract, setContract] = React.useState()

  // Fetch transactions and setup contract
  useEffect(() => {
    async function fetchTransactionsAndContract() {
      const transactions = await getAllPastTransactions();
      const contract = await getContract();
      
      await addActionToActivity(transactions, activity);

      setContract(contract)
    }
  }, [])

  // Once the contract is set in state, attach the event listener. 
  useEffect(() => {
    if (contract) {
      const handleTransfer = async (from, to, amount, event) => {
        console.log(`${from} sent ${ethers.utils.formatEther(amount)} to ${to}`);
        const transaction = await formatEventToTransaction(event);
        await addActionToActivity(transaction, activity);
      }

      contract.on('Transfer', handleTransfer);

      // Remove event listener, I imagine it will be something like
      return () => {
        contract.off('Transfer', handleTransfer)
      }
    }
    // Add contract and activity to the dependencies array.
  }, [contract, activity, selectedAsset, connectedNetwork, selectedWallet]);
}

我还想指出,删除和重新附加事件侦听器完全没问题。

【讨论】:

    【解决方案2】:

    我假设您想向activity 对象添加新事务。我还假设您在 addActionToActivity 的某个地方调用 setState 并使用新状态(当前活动与新事务)。您需要访问最新的活动,但在您的关闭中它不是最新的。

    使用setState 并传递一个函数给它,它将接收当前状态:

    setState(prevState => {
      // add transactions to prevState.activity 
      return { ...prevState, activity: {...prevState.activity, transactions: ... }};
    });
    

    所以,在你的例子中:

    function useQueryEvents() {
      const { state: { connectedNetwork, selectedWallet },
      } = useContext(LocalContext);
    
      useEffect(() => {
        async function bootstrapQueryEvents() {
          // First get all the past transactions
          const transactions = await getAllPastTransactions();
          const contract = await getContract();
    
          // Now save them to context state via addActionToActivity
          await addActionToActivity(transactions);
    
          // Now that all the past transactions have been saved
          // listen to all incoming/outgoing transactions and
          // save to context state via addActionToActivity
          contract.on('Transfer', async (from, to, amount, event) => {
            console.log(`${from} sent ${ethers.utils.formatEther(amount)} to ${to}`);
            const transaction = await formatEventToTransaction(event);
            await addActionToActivity(transaction);
          });
        }
    
        bootstrapQueryEvents();
      }, [selectedAsset, connectedNetwork, selectedWallet]); // <- I've tried adding `activity` here
    }
    ...
    const addActionToActivity = (transactions) => {
      ...
      setState(prevState => {
      // add transactions to prevState.activity 
      return { ...prevState, activity: {...prevState.activity, transactions: ... }};
    });
    }
    

    【讨论】:

    • 如果这能解决您的问题,请告诉我...我不完全确定 addActionToActivity 在您的代码中的作用。
    【解决方案3】:

    使用useRef() 保存和更新活动,使用useState() 管理重新渲染;

    function useQueryEvents() {
      const [epoch, setEpoch] = useState(0);
      const { current: heap } = useRef({ activity: [], epoch });
    
      useEffect(() => {
        // Here you can setup your listener, and operate on
        // heap.activity, which will always have an up-to-date
        // list. Also, "heap" is guaranteed to be the same object
        // in each render, thus even it is included into dependencies
        // of useEffect() (e.g. to keep ESLint's Rules of Hooks happy,
        // the useEffect() still fires just once. "setEpoch" is also
        // stable across re-renders, as all state setters.
    
        // And whenever you decide to re-render the component you do:
        setEpoch(++heap.epoch);
        // Or you can do something smarter, e.g. dumping to the local
        // state the actual stuff you want to render in the text pass.
      }, [heap, setEpoch]);
    
      return (
        // whatever you need to render
      );
    }
    

    【讨论】:

      【解决方案4】:

      您的订阅代码似乎没问题

      问题是当activity改变时组件没有更新

      当上下文对象内部发生更改时,组件不会自动更新。组件只有在上下文对象替换为其他对象时才会更新。

      这是一个包装器,它返回带有.notify() 实现的上下文对象。

      const UpdatableContext = ({ children }) => {
        // real value
        const [contextValue, setContextValue] = useState({});
        // add notify function
        // also cache result to avoid unnecessary component tree updates
        const mutableContextValue = useMemo(() => {
          let mutableContext = {
            ...contextValue,
            notify() {
              setContextValue(...value);
            },
          }
          return mutableContext;
        }, [contextValue]);
        return <Context.Provider value={mutableContextValue}>
          {children}
        </Context.Provider>;
      };
      

      在这样的上下文中更新你想要的任何东西,然后调用.notify() 来触发所有依赖组件的更新

      【讨论】:

        猜你喜欢
        • 2021-07-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-07-31
        • 2019-12-09
        • 1970-01-01
        • 1970-01-01
        • 2021-05-10
        相关资源
        最近更新 更多