How to compare oldValues and newValues on React Hooks useEffect?
Asked Answered
F

17

394

Let's say I have 3 inputs: rate, sendAmount, and receiveAmount. I put that 3 inputs on useEffect diffing params. The rules are:

  • If sendAmount changed, I calculate receiveAmount = sendAmount * rate
  • If receiveAmount changed, I calculate sendAmount = receiveAmount / rate
  • If rate changed, I calculate receiveAmount = sendAmount * rate when sendAmount > 0 or I calculate sendAmount = receiveAmount / rate when receiveAmount > 0

Here is the codesandbox https://codesandbox.io/s/pkl6vn7x6j to demonstrate the problem.

Is there a way to compare the oldValues and newValues like on componentDidUpdate instead of making 3 handlers for this case?

Thanks


Here is my final solution with usePrevious https://codesandbox.io/s/30n01w2r06

In this case, I cannot use multiple useEffect because each change is leading to the same network call. That's why I also use changeCount to track the change too. This changeCount also helpful to track changes from local only, so I can prevent unnecessary network call because of changes from the server.

Fortna answered 23/11, 2018 at 11:40 Comment(2)
How exactly is componentDidUpdate supposed to help? You will still need to write these 3 conditions.Dissipate
I added an answer with 2 optional solutions. Does one of them work for you?Nea
L
503

You can write a custom hook to provide you a previous props using useRef

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

and then use it in useEffect

const Component = (props) => {
    const {receiveAmount, sendAmount } = props
    const prevAmount = usePrevious({receiveAmount, sendAmount});
    useEffect(() => {
        if(prevAmount.receiveAmount !== receiveAmount) {

         // process here
        }
        if(prevAmount.sendAmount !== sendAmount) {

         // process here
        }
    }, [receiveAmount, sendAmount])
}

However its clearer and probably better and clearer to read and understand if you use two useEffect separately for each change id you want to process them separately

Lunate answered 23/11, 2018 at 12:22 Comment(17)
I tried the code above, but eslint is warning me that useEffect missing dependencies prevAmountCur
Since prevAmount holds value for the previous value of state/props, you don't need to pass it as a dependecy to useEffect and you can disable this warning for the particular case. You can read this post for more details ?Lunate
@ShubhamKhatri: What's the reason for the useEffect wrapping of ref.current = value?Philana
@Philana refs do not change references between renders and since we are mutating the ref object by setting the current key if won't cause a re-render thereby providing the previous value during the next re-renderLunate
also nicely explained here blog.logrocket.com/…Aeroembolism
Would && work or will useEffect run once for each variable at different times? if (prevAmount.receiveAmount !== receiveAmount && prevAmount.sendAmount !== sendAmount)Nocturn
In usePrevious, shouldn't the useEffect have a dependency on value? Otherwise if the component is re-rendered due to a different state change, in the next render, previousValue will equal value, right? Or am I missing something?Taeniafuge
this is awesome! but it won't only work for the first time the usePrevious is used ?Gooden
react becomes more and more awful: people are using useRef just for receiving previous props (!) and don't care how much it will cost.Underbodice
It's beautiful, because useRef persists same value for 2 consecutive renderings even if the dependency changes. As a results, previous value is preserved.Abranchiate
There's no need for usePrevious() function if the code in Component function was to be as above. So, setting useRef value in Component function useEffect will do the job. const Component = (props) => { const {someProp} = props; const ref = useRef(); //the ref value won't change when state is changed. useEffect(() => { if(ref.current !== someProp){ //... ref.current = someProp; } }, [someProp]); }Bedfellow
I still don't get why we don't need to pass prevAmount in the useEffect as dependency. if the amount changes multiple times, and we don't put the prevAmount in the dependency, won't it only hold the first value (instead of the 2nd, 3rd, and so on) in the useEffect? @ShubhamKhatri Thanks for your helpTrina
What happens if you got another unrelated state that rerenders the component? I think It's not gonna work unless you use something like this: github.com/streamich/react-use/blob/master/docs/…Houseroom
Someone needs to convince me that this programming model is better than prevProps and prevState.Ulphiah
This variation of usePrev hook has some drawbacks. Check out the improved version https://mcmap.net/q/86655/-how-to-compare-oldvalues-and-newvalues-on-react-hooks-useeffectGiaour
Calling a hook inside another hook?Cepheus
Drawback of usePrevious hook: if your component using this hook re-renders, with the value still being the same, the hook shows the new value. The reason being, usePrevious renders with an undefined value intitially and then the useEffect is run; which stores the latest value.Zolnay
B
154

In case anybody is looking for a TypeScript version of usePrevious:

In a .tsx module:

import { useEffect, useRef } from "react";

const usePrevious = <T extends unknown>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

Or in a .ts module:

import { useEffect, useRef } from "react";

const usePrevious = <T>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
Bevash answered 29/8, 2019 at 9:6 Comment(16)
Note that this will not work in a TSX file, to get this to work in a TSX file change to const usePrevious = <T extends any>(... to make the interpreter see think that <T> is not JSX and is a generic restrictionExhibit
Can you explain why <T extends {}> will not work. It seems to be working for me but I am trying to understand the complexity of using it like that.Closeup
.tsx files contain jsx which is meant to be identical to html. It's possible that the generic <T> is an unclosed component tag.Bevash
What about return type? I get Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type)Chromatolysis
@SabaAhang Sorry! TS will use type inference to handle return types for the most part, but if you have linting enabled I've added a return type to get rid of the warnings.Bevash
@SabaAhang I would strongly suggest using VSCode for TS work, also if you hold control and click on function names you get their EXACT types, so things like this are trivial to find. The TS language server does a great job of docs and type signatures.Bevash
It's better to extend unknown, or to simply put the hook in a .ts file. If you extend {} you will get errors if you skip specifying T in usePrevious<T>.Haro
@Haro Since writing this post, I've learned a lot about TS. I'll need to update this to reflect that. Thanks for the comment!Bevash
np, and yeah, always nice to keep it fresh/correct/up-to-date since many thousand people use these answers as references :)Haro
@Haro Updated my answer. Thanks for the feedback again.Bevash
To make it work in a TSX file you can also write <T,> to distinguish from JSX syntax (focus on the trailing comma)Densimeter
@Densimeter Is this a valid in older versions of tsc I don't think I've seen itBevash
Why not pass props as initial value, so result will be just T? Without possible undefined. const ref = useRef<T>(value);Teem
This variation of usePrev hook has some drawbacks. Check out the improved version https://mcmap.net/q/86655/-how-to-compare-oldvalues-and-newvalues-on-react-hooks-useeffectGiaour
Constraining the generic type T to unknown does nothing and is unnecessary. Change <T extends unknown> for <T,>Middaugh
To cite EsLint, "Constraining the generic type T to any does nothing and is unnecessary"Mellott
N
65

Option 1 - run useEffect when value changes

const Component = (props) => {

  useEffect(() => {
    console.log("val1 has changed");
  }, [val1]);

  return <div>...</div>;
};

Demo

Option 2 - useHasChanged hook

Comparing a current value to a previous value is a common pattern, and justifies a custom hook of it's own that hides implementation details.

const Component = (props) => {
  const hasVal1Changed = useHasChanged(val1)

  useEffect(() => {
    if (hasVal1Changed ) {
      console.log("val1 has changed");
    }
  });

  return <div>...</div>;
};

const useHasChanged= (val: any) => {
    const prevVal = usePrevious(val)
    return prevVal !== val
}

const usePrevious = (value) => {
    const ref = useRef();
    useEffect(() => {
      ref.current = value;
    });
    return ref.current;
}


Demo

Nea answered 29/6, 2019 at 11:58 Comment(3)
Second option worked for me. Can you please guide me why I have to write useEffect Twice?Vernievernier
@TarunNagpal, you don't necessarily need to use useEffect twice. It depends on your use case. Imagine that we just want to log which val has changed. If we have both val1 and val2 in our array of dependencies, it will run each time any of the values has changed. Then inside the function we pass we'll have to figure out which val has changed to log the correct message.Nea
Thanks for the reply. When you say "inside the function we pass we'll have to figure out which val has change". How we can achieve it. Please guideVernievernier
A
47

Going off the accepted answer, an alternative solution that doesn't require a custom hook:

const Component = ({ receiveAmount, sendAmount }) => {
  const prevAmount = useRef({ receiveAmount, sendAmount }).current;

  useEffect(() => {
    if (prevAmount.receiveAmount !== receiveAmount) {
     // process here
    }

    if (prevAmount.sendAmount !== sendAmount) {
     // process here
    }

    return () => { 
      prevAmount.receiveAmount = receiveAmount;
      prevAmount.sendAmount = sendAmount;
    };
  }, [receiveAmount, sendAmount]);
};

This assumes you actually need reference to the previous values for anything in the "process here" bits. Otherwise unless your conditionals are beyond a straight !== comparison, the simplest solution here would just be:

const Component = ({ receiveAmount, sendAmount }) => {
  useEffect(() => {
     // process here
  }, [receiveAmount]);

  useEffect(() => {
     // process here
  }, [sendAmount]);
};
Arv answered 8/4, 2020 at 20:9 Comment(2)
This will give you the initial values rather than the previous values. The reason the usePrevious() hook works is because it's wrapped in useEffect()Err
@Err The return function should update the previous values ref every render. Have you tested it?Arv
N
9

I just published react-delta which solves this exact sort of scenario. In my opinion, useEffect has too many responsibilities.

Responsibilities

  1. It compares all values in its dependency array using Object.is
  2. It runs effect/cleanup callbacks based on the result of #1

Breaking Up Responsibilities

react-delta breaks useEffect's responsibilities into several smaller hooks.

Responsibility #1

Responsibility #2

In my experience, this approach is more flexible, clean, and concise than useEffect/useRef solutions.

Nitriding answered 12/12, 2019 at 15:20 Comment(2)
Just what I'm looking for. Gonna try it out!Selfpronouncing
@Hisato it very well might be overkill. It's something of an experimental API. And frankly I haven't used it much within my teams because it's not widely known or adopted. In theory it sounds kind of nice, but in practice it might not be worth it.Nitriding
M
7

Here's a custom hook that I use which I believe is more intuitive than using usePrevious.

import { useRef, useEffect } from 'react'

// useTransition :: Array a => (a -> Void, a) -> Void
//                              |_______|  |
//                                  |      |
//                              callback  deps
//
// The useTransition hook is similar to the useEffect hook. It requires
// a callback function and an array of dependencies. Unlike the useEffect
// hook, the callback function is only called when the dependencies change.
// Hence, it's not called when the component mounts because there is no change
// in the dependencies. The callback function is supplied the previous array of
// dependencies which it can use to perform transition-based effects.
const useTransition = (callback, deps) => {
  const func = useRef(null)

  useEffect(() => {
    func.current = callback
  }, [callback])

  const args = useRef(null)

  useEffect(() => {
    if (args.current !== null) func.current(...args.current)
    args.current = deps
  }, deps)
}

You'd use useTransition as follows.

useTransition((prevRate, prevSendAmount, prevReceiveAmount) => {
  if (sendAmount !== prevSendAmount || rate !== prevRate && sendAmount > 0) {
    const newReceiveAmount = sendAmount * rate
    // do something
  } else {
    const newSendAmount = receiveAmount / rate
    // do something
  }
}, [rate, sendAmount, receiveAmount])

Hope that helps.

Martingale answered 8/5, 2020 at 13:22 Comment(1)
This is the best answer for what I need thanks!Mufi
M
7

If you prefer a useEffect replacement approach:

const usePreviousEffect = (fn, inputs = []) => {
  const previousInputsRef = useRef([...inputs])

  useEffect(() => {
    fn(previousInputsRef.current)
    previousInputsRef.current = [...inputs]
  }, inputs)
}

And use it like this:

usePreviousEffect(
  ([prevReceiveAmount, prevSendAmount]) => {
    if (prevReceiveAmount !== receiveAmount) // side effect here
    if (prevSendAmount !== sendAmount) // side effect here
  },
  [receiveAmount, sendAmount]
)

Note that the first time the effect executes, the previous values passed to your fn will be the same as your initial input values. This would only matter to you if you wanted to do something when a value did not change.

Misshapen answered 12/10, 2020 at 14:24 Comment(0)
D
6

Since state isn't tightly coupled with component instance in functional components, previous state cannot be reached in useEffect without saving it first, for instance, with useRef. This also means that state update was possibly incorrectly implemented in wrong place because previous state is available inside setState updater function.

This is a good use case for useReducer which provides Redux-like store and allows to implement respective pattern. State updates are performed explicitly, so there's no need to figure out which state property is updated; this is already clear from dispatched action.

Here's an example what it may look like:

function reducer({ sendAmount, receiveAmount, rate }, action) {
  switch (action.type) {
    case "sendAmount":
      sendAmount = action.payload;
      return {
        sendAmount,
        receiveAmount: sendAmount * rate,
        rate
      };
    case "receiveAmount":
      receiveAmount = action.payload;
      return {
        sendAmount: receiveAmount / rate,
        receiveAmount,
        rate
      };
    case "rate":
      rate = action.payload;
      return {
        sendAmount: receiveAmount ? receiveAmount / rate : sendAmount,
        receiveAmount: sendAmount ? sendAmount * rate : receiveAmount,
        rate
      };
    default:
      throw new Error();
  }
}

function handleChange(e) {
  const { name, value } = e.target;
  dispatch({
    type: name,
    payload: value
  });
}

...
const [state, dispatch] = useReducer(reducer, {
  rate: 2,
  sendAmount: 0,
  receiveAmount: 0
});
...
Dissipate answered 23/11, 2018 at 12:45 Comment(6)
Hi @estus, thanks for this idea. It gives me another way of thinking. But, I forgot to mention that I need to call API for each of the case. Do you have any solution for that?Fortna
What do you mean? Inside handleChange? Does 'API' mean remote API endpoint? Can you update codesandbox from the answer with details?Dissipate
From the update I can assume that you want to fetch sendAmount, etc asynchronously instead of calculating them, right? This changes a lot. It's possible to do this with useReduce but may be tricky. If you dealt with Redux before you may know that async operations aren't straightforward there. github.com/reduxjs/redux-thunk is a popular extension for Redux that allows for async actions . Here's a demo that augments useReducer with same pattern (useThunkReducer), codesandbox.io/s/6z4r79ymwr . Notice that dispatched function is async, you can do requests there (commented).Dissipate
The problem with the question is that you asked about a different thing that wasn't your real case and then changed it, so now it's very different question while existing answers appear as if they just ignored the question. This isn't a recommended practice on SO because it doesn't help you to get an answer you want. Please, don't remove important parts from the question that answers already refer to (calculation formulas). If you still have problems (after my previous comment (you likely do), consider asking a new question that reflects your case and link to this one as your previous attempt.Dissipate
Hi estus, I think this codesandbox codesandbox.io/s/6z4r79ymwr is not updated yet. I will change back the question, sorry for that.Fortna
Yes, it didn't save automatically after forking. I updated it.Dissipate
H
4

For really simple prop comparison you can use useEffect to easily check to see if a prop has updated.

const myComponent = ({ prop }) => {
  useEffect(() => {
    ---Do stuffhere----
  }, [prop])
}

useEffect will then only run your code if the prop changes.

Heyer answered 20/9, 2019 at 9:29 Comment(6)
This only works once, after consecutive prop changes you will not be able to ~ do stuff here ~Ameline
Seems like you should call setHasPropChanged(false) at the end of ~ do stuff here ~ to "reset" your state. (But this would reset in an extra rerender)Oma
Thanks for the feedback, you are both right, updated solutionHeyer
@AntonioPavicevac-Ortiz I've updated the answer to now render the propHasChanged as true which would then call it once on render, might be a better solution just to rip out the useEffect and just check the propHeyer
I think my original use of this has been lost. Looking back at the code you can just use useEffectHeyer
This is simple and smart. Why didn't I think of it lol :) Good job.Laforge
G
4

Be careful with most voted answers. For more complex scenarios above variations of usePrevious can give you too much re-renders (1) or the same value as original (2).

We have to:

  1. Add [value] as dependency in useEffect to re-run only if value changes
  2. Assign JSON.parse(JSON.stringify(value)) (or some deep copy) to ref.current insinde useEffect to prevent passing the reference of state to ref instead of the value

Upgraded hook:

const usePrevious = <T>(value: T): T => {
  const ref: any = useRef<T>()

  useEffect(() => {
    ref.current = JSON.parse(JSON.stringify(value))
  }, [value])

  return ref.current
}
Giaour answered 22/4, 2022 at 9:15 Comment(1)
useRef doesn't trigger an extra rerender - check this codesandbox codesandbox.io/p/sandbox/… (P.S: the color doesn't change on first click because it doesn't trigger a state update)Graiggrail
T
3

Using Ref will introduce a new kind of bug into the app.

Let's see this case using usePrevious that someone commented before:

  1. prop.minTime: 5 ==> ref.current = 5 | set ref.current
  2. prop.minTime: 5 ==> ref.current = 5 | new value is equal to ref.current
  3. prop.minTime: 8 ==> ref.current = 5 | new value is NOT equal to ref.current
  4. prop.minTime: 5 ==> ref.current = 5 | new value is equal to ref.current

As we can see here, we are not updating the internal ref because we are using useEffect

Thermogenesis answered 28/11, 2019 at 15:32 Comment(1)
React has this example but they are using the state... and not the props... when you care about the old props then error will happened.Thermogenesis
I
0

Here's a Typescript version Aadit M Shah's Answer.

I renamed it from useTransition to usePrevious since useTransition already exists in React.

import { useEffect, useRef, useState } from 'react';

const usePrevious = <T extends any[],>(callback: (prev: T) => void, deps: T): void => {
  const callbackRef = useRef<null | ((prev: T) => void)>(null);

  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  const depsRef = useRef<null | T>(null);

  const [initial, setInitial] = useState(true);

  useEffect(() => {
    if (!initial && depsRef.current !== null && callbackRef.current !== null) {
      callbackRef.current(depsRef.current);
    }

    depsRef.current = deps;
    setInitial(false);
  }, deps);
}

export default usePrevious;

Usage:

  usePrevious<[boolean]>(([prevIsOpen]) => {
    console.log('prev', prevIsOpen);
    console.log('now', isOpen);
  }, [isOpen])
Institution answered 6/7, 2022 at 0:33 Comment(1)
don't you think it's complicated?Duodenal
Z
0

There seems to be an issue with the accepted answer

Problem with the accepted answer:

If your component parent component re-renders, with the value still being the same, the usePrevious hook shows the new value.

My solution

This fixes the issue so the previous value persists if the parent component is re-rendered.

export const usePrevious = <T>(value: T) => {
const previousValue = useRef<T>();
const newValue = useRef<T>();

if (newValue.current !== value) {
    previousValue.current = newValue.current;
    newValue.current = value;
}

return previousValue.current;

};

Zolnay answered 19/9, 2023 at 15:28 Comment(0)
C
-1

You can use useImmer opposed to useState and access the state. Example: https://css-tricks.com/build-a-chat-app-using-react-hooks-in-100-lines-of-code/

Cesarcesare answered 18/10, 2020 at 3:57 Comment(0)
E
-1

I did not like any of the answers above, I wanted the ability to pass an array of booleans and if one of them is true so rerender

/**
 * effect fires if one of the conditions in the dependency array is true
 */
export const useEffectCompare = (callback: () => void, conditions: boolean[], effect = useEffect) => {
  const shouldUpdate = useRef(false);
  if (conditions.some((cond) => cond)) shouldUpdate.current = !shouldUpdate.current;
  effect(callback, [shouldUpdate.current]);
};

//usage - will fire because one of the dependencies is true.
useEffectCompare(() => {
  console.log('test!');
}, [false, true]);
Eve answered 25/7, 2021 at 10:12 Comment(0)
B
-1

This is how I managed to do it in Typescript:

import { useEffect, useRef } from "react";

export default function usePrevious<T extends any[]>(fn: (prev: T) => any, deps: T) {
  const previousDepsRef = useRef<T>(deps);

  useEffect(() => {
    const eject = fn(previousDepsRef.current);
    previousDepsRef.current = deps;
    return eject;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps);
}

Then use it like this:

usePrevious(([prevCount]) => {
  console.log(prevCount, count);
}, [count]);
Bjorn answered 30/5, 2023 at 6:28 Comment(0)
H
-2

In your case(simple object):

useEffect(()=>{
  // your logic
}, [rate, sendAmount, receiveAmount])

In other case(complex object)

const {cityInfo} = props;
useEffect(()=>{
  // some logic
}, [cityInfo.cityId])
Horoscopy answered 28/12, 2020 at 9:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.