最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Update state within listener that is inside useEffect - Stack Overflow

programmeradmin4浏览0评论

I have a hook called useQueryEvents that 1) fetches all past transactions for a user and 2) listens to the network for ining/outgoing transactions. 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.

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. 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.

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 ining/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? Or maybe there's a different approach I can take altogether?

Thanks in advance

I have a hook called useQueryEvents that 1) fetches all past transactions for a user and 2) listens to the network for ining/outgoing transactions. 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.

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. 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.

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 ining/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? Or maybe there's a different approach I can take altogether?

Thanks in advance

Share Improve this question edited May 9, 2021 at 19:44 WillKre asked May 3, 2021 at 14:15 WillKreWillKre 6,1686 gold badges36 silver badges64 bronze badges 3
  • Could you please post the code used in addActionToActivity? Just wanted to check how it handles transaction arrays and transactions. Also when you mean it's off by one, does that mean that the contract.on does trigger re-renders? – nipuna-g Commented May 5, 2021 at 17:05
  • Without seeing what's in addActionToActivity, we can't really help you. How do you update the state? Do you modify the state directly? – jperl Commented May 12, 2021 at 15:54
  • How e none of the dependencies are used in the useEffect? Where is selectedAsset ing from? – jperl Commented May 12, 2021 at 16:02
Add a ment  | 

4 Answers 4

Reset to default 4 +250

You could solve this by splitting your logic into two 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.

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). 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(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 ining/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; i.e.

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 ponent 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 ponent 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.

This is wrapper which returns context object with .notify() implementation.

const UpdatableContext = ({ children }) => {
  // real value
  const [contextValue, setContextValue] = useState({});
  // add notify function
  // also cache result to avoid unnecessary ponent 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 ponents

发布评论

评论列表(0)

  1. 暂无评论