Search code examples
reactjsreact-reduxreact-hooksuse-effect

How to compare oldValues and newValues on React Hooks useEffect? Multiple re-renders


The kinda the same problem as described here How to compare oldValues and newValues on React Hooks useEffect?

But in my case, usePrevious hook does not help.

Imagine the form with several inputs, selects, and so on. You may want to look at https://app.uniswap.org/#/swap to make a similar visualization. There are several actions and data updates that will be happened on almost any change, which will lead to several re-renders, at least 4. For example.

I have 2 inputs, each represents a token. Base(first one) and Quote(second one).

This is a state for Base

const [base, setBase] = useState({
    balance: undefined,
    price: undefined,
    value: initState?.base?.value,
    token: initState?.base?.token,
    tokenId: initState?.base?.tokenId,
});

and for Quote

const [quote, setQuote] = useState({
    balance: undefined,
    price: undefined,
    value: initState?.quote?.value,
    token: initState?.quote?.token,
    tokenId: initState?.quote?.tokenId,
});

They gonna form a pair, like BTC/USD for example.

By changing token (instead of BTC I will choose ETH) in the select menu I will trigger several actions: fetching wallet balance, fetching price, and there are gonna be a few more rerenders with input view update and modal window close. So at least 4 of them are happening right now. I want to be able to compare base.token and basePrv with const basePrv = usePrevious(base?.token); but on the second re-render base.token and basePrv gonna have the same token property already and it is an issue.

I also have the swap functionality between the inputs where I should change base with quote and quote with base like that

setBase(prevState => ({
    ...prevState,
    base: quote
}));

setQuote(prevState => ({
    ...prevState,
    quote: base
}));

In that case, there are no additional requests that should be triggered.

Right now I have useEffect with token dependency on it. But it will be fired each time when the token gonna be changed which will lead to additional asynchronous calls and 'tail' of requests if you gonna click fast. That's why I need to compare the token property that was before the change to understand should I make additional calls and requests because of the formation of new pair (BTC/USD becomes ETH/USD) or I should ignore that because it was just a "swap" (BTC/USD becomes USD/BTC) and there is no need to make additional calls and fetches. I just had to, well, swap them, not more.

So in my story, usePrevious hook will return the previous token property only once, and at the second and third time, it would be overwritten by multiple re-renders(other properties would be fetched) to the new one. So at the time when useEffect gonna be triggered, I would have no chance to compare the previous token property and the current one, because they will show the same.

I have several thoughts on how to solve it, but I am not sure is it right or wrong, because it seemed to me that the decisions look more imperative than declarative.

  1. I can leave everything as it is (requests would be triggered always on any change no matter what it was. Was it a swap or user changed a pair). I can disable the swap button until all of the requests would be finished. It would solve the problem with requests 'tail'. But it is a kinda hotfix, that gonna be work, but I do not like it, because it would lead to additional unnecessary requests and it would be slow and bad for UX.

  2. I can use a state to keep the previous pair on it right before the update by setBase or setQuote happens. It will allow me to use useEffect and compare previous pair to the current one to understand did the pair was changed, or just swapped and take the decision should I make fetches and calls or not.

  3. I can get rid of useEffect with base.token and quote.token dependencies and handle everything inside of onClick handler. Because of that, the swap functionality would not trigger useEffect, and calls and fetches would be fired only if the user gonna click and choose something different. But as I said this option seemed a little bit odd to me.

  4. I tried to use closure here, to "remember" the previous state of tokens, but it is kinda similar to use the current component state. Also, you have to initialize closure outside of the functional component body, and I do not see a possibility to transfer the init state into it that way, so the code becomes more spaghettified.

So any other ideas guys? I definitely missing something. Maybe that much of re-renders is an antipattern but I am not sure how to avoid that.


Solution

  • There could be multiple solutions to your problem. I would suggest to pick one which is easier to understand.

    1. Modify the usePrevious hook

    You can modify the usePrevious hook to survive multiple renders.

    Tip: use JSON.stringify to compare if you think the value will be a complex object and might change the reference even for same real value.

    function usePrevious(value) {
      const prevRef = useRef();
      const curRef = useRef();
    
      if (value !== curRef.current){
      // or, use
      // if ( JSON.stringify(value) !== JSON.stringify(curRef.current)){
        prevRef.current = curRef.current;
        curRef.current = value;
      }
      
      return prevRef.current;
    }
    

    2. Sort useEffect dependency array

    Since you're using tokens(strings) as dependency array of useEffect, and you don't mind their order (swap shouldn't change anything), sort the dependency array like

    useEffect(
      () => {
        // do some effect
      },
      [base.token, quote.token].sort()
    )
    

    3. Store the currently fetched tokens.

    While storing the API response data, also store the tokens(part of request) associated with that data. Now, you'll have 2 sets of tokens.

    1. currently selected tokens
    2. currently fetched tokens

    You can chose to fetch only when the currently fetched tokens don't fulfil your needs. You can also extend this and store previous API request/responses and pick the result from them if possible.

    Verdict

    Out of all these, 3rd seems a nice & more standardised approach to me, but an overkill for your need (unless you want to cache previous results).

    I would have gone with 2nd because of simplicity and minimalism. However, It still depends on what you find easier at the end.