ChatGPT解决这个技术问题 Extra ChatGPT

How to compare oldValues and newValues on React Hooks useEffect?

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.

How exactly is componentDidUpdate supposed to help? You will still need to write these 3 conditions.
I added an answer with 2 optional solutions. Does one of them work for you?

S
Shubham Khatri

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


Thanks for the note on using two useEffect calls separately. Wasn't aware you could use it multiple times!
I tried the code above, but eslint is warning me that useEffect missing dependencies prevAmount
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 ?
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?
react becomes more and more awful: people are using useRef just for receiving previous props (!) and don't care how much it will cost.
S
SeedyROM

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

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 restriction
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.
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>.
@fgblomqvist Updated my answer. Thanks for the feedback again.
To make it work in a TSX file you can also write to distinguish from JSX syntax (focus on the trailing comma)
B
Ben Carp

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


Second option worked for me. Can you please guide me why I have to write useEffect Twice?
@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.
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
D
Drazen Bjelovuk

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

Good solution, but contains syntax errors. useRef not userRef. Forgot to use current prevAmount.current
This is super cool, if I could upvote more I would! Makes total sense as well, my original answer came from a place of ignorance. I think this is probably the simplest and most elegant pattern I've seen so far.
You could probably abstract this even further to make a super simple utility.
@Justin The return function should update the previous values ref every render. Have you tested it?
That's a super clean answer, i'd personaly choose this solution, it works like a charm on my project. Thank you !
A
Austin Malerba

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

Responsibilities

It compares all values in its dependency array using Object.is 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

usePrevious(value)

useLatest(value)

useDelta(value, options)

useDeltaArray(valueArray, options)

useDeltaObject(valueObject, options)

some(deltaArray)

every(deltaArray)

Responsibility #2

useConditionalEffect(callback, boolean)

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


Just what I'm looking for. Gonna try it out!
@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.
J
Joe Van Leeuwen

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.


E
Estus Flask

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

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?
What do you mean? Inside handleChange? Does 'API' mean remote API endpoint? Can you update codesandbox from the answer with details?
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).
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.
Hi estus, I think this codesandbox codesandbox.io/s/6z4r79ymwr is not updated yet. I will change back the question, sorry for that.
A
Aadit M Shah

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.


This is the best answer for what I need thanks!
i
ib.

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.


This only works once, after consecutive prop changes you will not be able to ~ do stuff here ~
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)
Thanks for the feedback, you are both right, updated solution
@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
I think my original use of this has been lost. Looking back at the code you can just use useEffect
s
santomegonzalo

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

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

prop.minTime: 5 ==> ref.current = 5 | set ref.current prop.minTime: 5 ==> ref.current = 5 | new value is equal to ref.current prop.minTime: 8 ==> ref.current = 5 | new value is NOT equal to ref.current 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


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

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:

Add [value] as dependency in useEffect to re-run only if value changes 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
}

j
joemillervi

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/


E
Eliav Louski

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

j
jakub_jo

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

J
JChen___

In your case(simple object):

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

In other case(complex object)

const {cityInfo} = props;
useEffect(()=>{
  // some logic
}, [cityInfo.cityId])