385

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.

Erwin Zhang
  • 4,085
  • 2
  • 16
  • 11

16 Answers16

493

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

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • 10
    I tried the code above, but eslint is warning me that `useEffect` missing dependencies `prevAmount` – Littlee May 20 '19 at 09:53
  • 3
    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 ? – Shubham Khatri May 20 '19 at 10:10
  • @ShubhamKhatri: What's the reason for the useEffect wrapping of ref.current = value? – curly_brackets Jan 30 '20 at 06:26
  • @curly_brackets 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-render – Shubham Khatri Jan 30 '20 at 11:28
  • 1
    also nicely explained here https://blog.logrocket.com/how-to-get-previous-props-state-with-react-hooks/ – IsmailS Jan 31 '20 at 21:10
  • Would `&&` work or will useEffect run once for each variable at different times? `if (prevAmount.receiveAmount !== receiveAmount && prevAmount.sendAmount !== sendAmount)` – Ryan Walker Feb 17 '20 at 21:56
  • 14
    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? – azizj Mar 19 '20 at 21:31
  • this is awesome! but it won't only work for the first time the usePrevious is used ? – Ido Bleicher Apr 26 '20 at 16:21
  • 12
    `react` becomes more and more awful: people are using `useRef` just for receiving previous props (!) and don't care how much it will cost. – puchu May 31 '21 at 22:40
  • It's beautiful, because **useRef** persists same value for 2 consecutive renderings even if the dependency changes. As a results, previous value is preserved. – Kavindu Vindika Aug 15 '21 at 01:52
  • 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]); } – Chirag Dave Oct 31 '21 at 04:06
  • 1
    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 help – Robert Tirta Dec 13 '21 at 05:01
  • 1
    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: https://github.com/streamich/react-use/blob/master/docs/usePreviousDistinct.md – gadi tzkhori Feb 04 '22 at 09:24
  • Someone needs to convince me that this programming model is better than prevProps and prevState. – cherouvim Mar 11 '22 at 13:02
  • This variation of `usePrev` hook has some drawbacks. Check out the improved version https://stackoverflow.com/a/71966286/7268884 – twistezo Apr 22 '22 at 09:16
  • Calling a hook inside another hook? – Siraj Alam Jun 24 '23 at 07:51
147

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;
};
isherwood
  • 58,414
  • 16
  • 114
  • 157
SeedyROM
  • 2,203
  • 1
  • 18
  • 22
  • 5
    Note that this will not work in a TSX file, to get this to work in a TSX file change to `const usePrevious = (...` to make the interpreter see think that is not JSX and is a generic restriction – apokryfos Oct 23 '19 at 12:23
  • 2
    Can you explain why `` will not work. It seems to be working for me but I am trying to understand the complexity of using it like that. – Karthikeyan_kk Dec 12 '19 at 09:58
  • `.tsx` files contain jsx which is meant to be identical to html. It's possible that the generic `` is an unclosed component tag. – SeedyROM Dec 12 '19 at 20:55
  • What about return type? I get `Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type)` – Saba Ahang Feb 21 '20 at 17:59
  • @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. – SeedyROM Mar 09 '20 at 18:42
  • @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. – SeedyROM Mar 09 '20 at 18:45
  • 1
    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`. – fgblomqvist Aug 19 '20 at 17:52
  • @fgblomqvist Since writing this post, I've learned a lot about TS. I'll need to update this to reflect that. Thanks for the comment! – SeedyROM Aug 19 '20 at 17:53
  • 1
    np, and yeah, always nice to keep it fresh/correct/up-to-date since many thousand people use these answers as references :) – fgblomqvist Aug 19 '20 at 18:26
  • 2
    @fgblomqvist Updated my answer. Thanks for the feedback again. – SeedyROM Aug 19 '20 at 22:50
  • 6
    To make it work in a TSX file you can also write to distinguish from JSX syntax (focus on the trailing comma) – Corvince May 05 '21 at 14:40
  • @Corvince Is this a valid in older versions of `tsc` I don't think I've seen it – SeedyROM Nov 02 '21 at 08:45
  • Why not pass props as initial value, so result will be just T? Without possible undefined. `const ref = useRef(value);` – acidernt Apr 06 '22 at 12:09
  • This variation of `usePrev` hook has some drawbacks. Check out the improved version https://stackoverflow.com/a/71966286/7268884 – twistezo Apr 22 '22 at 09:17
  • Constraining the generic type `T` to `unknown` does nothing and is unnecessary. Change for – Juan David Arce Mar 06 '23 at 07:06
63

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

Ben Carp
  • 24,214
  • 9
  • 60
  • 72
  • Second option worked for me. Can you please guide me why I have to write useEffect Twice? – Tarun Nagpal Mar 27 '20 at 16:51
  • 1
    @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. – Ben Carp Mar 28 '20 at 16:39
  • 1
    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 guide – Tarun Nagpal Mar 29 '20 at 17:54
46

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]);
};
isherwood
  • 58,414
  • 16
  • 114
  • 157
Drazen Bjelovuk
  • 5,201
  • 5
  • 37
  • 64
  • 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()` – Justin Jun 23 '21 at 20:20
  • 1
    @Justin The return function should update the previous values ref every render. Have you tested it? – Drazen Bjelovuk Jun 24 '21 at 03:19
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.

Austin Malerba
  • 169
  • 2
  • 2
  • Just what I'm looking for. Gonna try it out! – hackerl33t Apr 24 '20 at 10:34
  • @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. – Austin Malerba Jul 10 '20 at 15:41
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.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
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.

Joe Van Leeuwen
  • 266
  • 3
  • 8
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
});
...
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 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? – Erwin Zhang Nov 23 '18 at 13:05
  • What do you mean? Inside handleChange? Does 'API' mean remote API endpoint? Can you update codesandbox from the answer with details? – Estus Flask Nov 23 '18 at 13:13
  • 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. https://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), https://codesandbox.io/s/6z4r79ymwr . Notice that dispatched function is `async`, you can do requests there (commented). – Estus Flask Nov 23 '18 at 15:54
  • 1
    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. – Estus Flask Nov 23 '18 at 16:05
  • Hi estus, I think this codesandbox https://codesandbox.io/s/6z4r79ymwr is not updated yet. I will change back the question, sorry for that. – Erwin Zhang Nov 24 '18 at 04:17
  • Yes, it didn't save automatically after forking. I updated it. – Estus Flask Nov 24 '18 at 06:22
6

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
}
twistezo
  • 512
  • 1
  • 11
  • 24
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.

ib.
  • 27,830
  • 11
  • 80
  • 100
Charlie Tupman
  • 519
  • 6
  • 11
  • 2
    This only works once, after consecutive `prop` changes you will not be able to `~ do stuff here ~` – Karolis.sh Oct 10 '19 at 07:34
  • 2
    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) – Kevin Wang Oct 16 '19 at 18:21
  • Thanks for the feedback, you are both right, updated solution – Charlie Tupman Jun 02 '20 at 14:44
  • @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 prop – Charlie Tupman Jun 11 '20 at 16:17
  • I think my original use of this has been lost. Looking back at the code you can just use useEffect – Charlie Tupman Jun 11 '20 at 16:20
  • This is simple and smart. Why didn't I think of it lol :) Good job. – Jay Mayu Jul 19 '20 at 12:43
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

  • 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. – santomegonzalo Nov 28 '19 at 16:27
0

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/

joemillervi
  • 1,009
  • 1
  • 8
  • 18
0

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]);
Eliav Louski
  • 3,593
  • 2
  • 28
  • 52
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])
jakub_jo
  • 1,494
  • 17
  • 22
-1

In your case(simple object):

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

In other case(complex object)

const {cityInfo} = props;
useEffect(()=>{
  // some logic
}, [cityInfo.cityId])
JChen___
  • 3,593
  • 2
  • 20
  • 12
-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]);
Hayyaun
  • 303
  • 3
  • 10