2

I want to store the previous value in a variable and use it in a function. Let's say if the current value is 9, the previous value is supposed to be 8 (like one less). The problem is that console.log(prevServings) returns undefined on the first render and shows the previous value on the second render but the difference between current and previous values are 2 instead of 1. My understanding is that the current value is not available on the first render so the previous value is undefined but I don't know how to fix it. Any help would be appreciated. Thanks in advance.

const Child = ({originalData}) =>{
  //clone original data object with useState
  const [copyData, setCopyDta] = useState({});
  //clone the copied data 
  let duplicate = {...copyRecipe};
  
  
  //store previous servings value into a variable
  const usePrevious = (servings) => {
    const ref = useRef();
    useEffect(() => {
      ref.current = servings;
    }, [servings]);

    return ref.current;
  };
  const prevServings = usePrevious(duplicate.servings);
  
  //increase the number of servings on click
  const incrementHandle = () => {
    duplicate.servings = `${parseInt(duplicate.servings) + 1}`;
    //this return undefined on the first render
    console.log(prevServings);
    setCopyRecipe(duplicate);
  }

  return(
    <p>{copyData.servings}</p>
    <Increment onClick={incrementHandle}/>
  )
}

0

3 Answers 3

6

It returns undefined because useEffect() isn't going to trigger until at least after the first render. You probably want to do this instead:

const usePrevious = (servings) => {
  const ref = useRef(servings);
  useEffect(() => {
    ref.current = servings;
  }, [servings])
  return ref.current;
}

This does feel like it would be hard to reason about, though. I would probably recommend using a reducer or regular state instead. A ref is useful if you don't want the component to 'react' to changes to that specific value, but every time the ref changes here you fire a state update anyway.

Sign up to request clarification or add additional context in comments.

1 Comment

When I set the default value as the value passed in, it always returns the current value when I use it instead of the previous value.
0

Issue

Oops, not quite a duplicate. The issue here is that since you've declared usePrevious inside the component it is recreated each render cycle, effectively negating any caching efforts.

Solution

Move the usePrevious hook outside the component body so it's a stable reference. You may want to also remove the useEffect's dependency so you are caching the value every render cycle.

//store previous servings value into a variable
const usePrevious = (value) => {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

const Child = ({originalData}) =>{
  //clone original data object with useState
  const [copyData, setCopyDta] = useState({});
  //clone the copied data 
  let duplicate = { ...copyData };
  
  const prevServings = usePrevious(duplicate.servings);
  
  //increase the number of servings on click
  const incrementHandle = () => {
    duplicate.servings = `${parseInt(duplicate.servings) + 1}`;
    setCopyDta(duplicate);
  }

  return(
    <p>{copyData.servings}</p>
    <Increment onClick={incrementHandle}/>
  )
}

FYI

Just an FYI, let duplicate = { ...copyRecipe }; is only a shallow copy and not a clone of the object, all nested properties will still be references back to objects in the original object. Maybe this is all you need, but just wanted to point out that this isn't a true clone of the object.

QnA

The problem is that console.log(prevServings) returns undefined on the first render and shows the previous value on the second render

I would this should be the expected behavior because on the initial render there was no previous render from which to cache a value.

but the difference between current and previous values are 2 instead of 1.

Regarding the "off-by-2" issue, from what I can tell you cache the unupdated shallow copy of duplicate each render, and when incrementHandle is clicked you log the prevServings value and then enqueue an update which triggers a rerender. The value you log and the state update result (i.e. <p>{copyData.servings}</p>) are two render cycles apart. If you compare both values at the same point in the render cycle you will see they are 1 apart.

useEffect(() => {
  console.log({ prevServings, current: copyData.servings })
})

Log output:

{prevServings: undefined, current: 0}
{prevServings: 0, current: "1"}
{prevServings: "1", current: "2"}
{prevServings: "2", current: "3"}
{prevServings: "3", current: "4"}

11 Comments

Thank you for your detailed answer. I followed your suggestion by creating a separate component called usePrevious and imported it back to the Child component. But by the first click it still returns undefined and I need it to be a number.
Also, thanks for the extra information, i looked it up online and found some a way to deep clone an object like {const cloneFood = JSON.parse(JSON.stringify(food));}
@Simon If there's no previous render then what would the previous value be? When you say you need it to be a number, are you referring to the type, or that you just need a defined value? Can you explain the use case where you need a defined previous value on the initial render?
I'm working on a recipe website, so I want the amount of ingredients to adjust to the number of servings. I'm thinking of an equation like: newIngredientAmount = ingredientAmount/prevServings * currentServings. So by default, the preServings has to be the default value of the recipe. For example: the default value is 8 and when you click the button it turns 9, so the prevServings is 8 and currentServings is 9. I hope that makes more sense to you.
|
0

Passing the initial value for ref.current to usePrevious hook should help:

function usePrevious(value, initialValue) {
  const ref = useRef(initialValue);

  useEffect(() => {
    ref.current = value;
  }, []);

  return ref.current;
}

on the first render, it will return what is passed in the second argument:

const INITIAL_VALUE = 0;
const previousValue = usePrevious(currentValue, INITIAL_VALUE);
console.log(previousValue); // 0

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.