简体   繁体   English

在 useEffect 内部的侦听器中更新 state

[英]Update state within listener that is inside useEffect

I have a hook called useQueryEvents that 1) fetches all past transactions for a user and 2) listens to the.network for incoming/outgoing transactions.我有一个名为useQueryEvents的挂钩,它 1) 为用户获取所有过去的交易,以及 2) 监听 .network 的传入/传出交易。 In both cases the transactions are passed into a function addActionToActivity that simply appends it to the activity array and updates it in the context state under the key activity .在这两种情况下,交易都被传递到 function addActionToActivity中,它只是将其附加到活动数组并在键activity下的上下文 state 中更新它。

I can't get the activity to sync correctly.我无法让活动正确同步。 Whenever the state updates it does not have the last transaction because it's always one step behind.每当 state 更新时,它都没有最后一笔交易,因为它总是落后一步。 If I add activity to the dependancy it works but then starts a new listener (due to the whole function being called again with the new activity value) which causes an infinity-like-loop which keeps switching up the state.如果我向依赖项添加activity ,它会起作用,但随后会启动一个新的侦听器(由于使用新activity值再次调用整个 function),这会导致类似无限的循环不断切换 state。

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
}

Any ideas how I can approach updating the state while having access to the updated activity value inside the listener without starting a new instance of the listener?有什么想法可以在启动侦听器的新实例的情况下访问侦听器内部更新的activity值的同时更新 state 吗? Or maybe there's a different approach I can take altogether?或者也许我可以完全采用不同的方法?

Thanks in advance提前致谢

You could solve this by splitting your logic into two useEffects.您可以通过将您的逻辑分成两个 useEffects 来解决这个问题。 Right now you do two things:现在你做两件事:

  1. You fetch transactions你获取交易
  2. Setup event listener设置事件监听器

The issue is that you cannot run this hook again without doing both things at the same time.问题是如果不同时做这两件事,你就不能再次运行这个钩子。 And as you stated, the activity object is not what you intend it to be, as the activity object is what is passed at the time the event listener is setup.正如您所说,活动 object 不是您想要的,因为活动 object 是在设置事件侦听器时传递的内容。

Splitting it into two hooks could look something like this:将它分成两个钩子可能看起来像这样:

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]);
}

I'd also like to point out that it's perfectly fine to remove and reattach event listeners.我还想指出,删除和重新附加事件侦听器完全没问题。

I assume you want to add new transactions to the activity object. I assume also you call setState somewhere in addActionToActivity with the new state (current activity with new transactions).我假设您想向activity object 添加新交易。我还假设您使用新的 state(当前活动与新交易)在addActionToActivity的某处调用setState You need to have access to the latest activity, but in your closure it's not the latest one.您需要访问最新的活动,但在您的关闭中它不是最新的。

Use setState and pass a function to it, which will receive the current state:使用setState并传递一个 function 给它,它会收到当前的 state:

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

So, in your example:所以,在你的例子中:

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: ... }};
});
}

Use useRef() to keep and update activities, and useState() to manage re-renders;使用useRef()来保持和更新活动,使用useState()来管理重新渲染; ie IE

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
  );
}

Your subscription code seems OK您的订阅代码似乎没问题

The problem is that component doesn't update when activity changes问题是组件在活动更改时不会更新

Component will not be updated automatically when something is changed inside context object. Component will update only when context object replaced with other one.当上下文 object 中的某些内容发生更改时,组件不会自动更新。只有当上下文 object 被其他上下文替换时,组件才会更新。

This is wrapper which returns context object with .notify() implementation.这是包装器,它返回带有.notify()实现的上下文 object。

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>;
};

Update anything you want within such context and then call .notify() to trigger update of all dependent components在这样的上下文中更新你想要的任何东西,然后调用.notify()来触发所有依赖组件的更新

I have a hook called useQueryEvents that 1) fetches all past transactions for a user and 2) listens to the network for incoming/outgoing transactions.我有一个名为useQueryEvents的钩子,它 1) 为用户获取所有过去的交易,2) 监听网络的传入/传出交易。 In both cases the transactions are passed into a function addActionToActivity that simply appends it to the activity array and updates it in the context state under the key activity .在这两种情况下,事务都被传递到 function addActionToActivity中,它只是将其附加到活动数组并在关键activity下的上下文 state 中更新它。

I can't get the activity to sync correctly.我无法让活动正确同步。 Whenever the state updates it does not have the last transaction because it's always one step behind.每当 state 更新时,它都没有最后一笔交易,因为它总是落后一步。 If I add activity to the dependancy it works but then starts a new listener (due to the whole function being called again with the new activity value) which causes an infinity-like-loop which keeps switching up the state.如果我向依赖项添加activity ,它会起作用,但随后会启动一个新的侦听器(由于使用新的activity值再次调用整个 function),这会导致无限循环,不断切换 state。

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
}

Any ideas how I can approach updating the state while having access to the updated activity value inside the listener without starting a new instance of the listener?有什么想法可以在启动侦听器的新实例的情况下访问侦听器内部更新的activity值的同时更新 state 吗? Or maybe there's a different approach I can take altogether?或者也许我可以完全采取不同的方法?

Thanks in advance提前致谢

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM