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}/>
  )
}
Simon
  • 47
  • 1
  • 9

2 Answers2

4

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.

Dan
  • 10,282
  • 2
  • 37
  • 64
  • 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. – Josh Dec 02 '21 at 13:25
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"}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • 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. – Simon Jun 18 '21 at 19:50
  • 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 Jun 18 '21 at 19:53
  • @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? – Drew Reese Jun 18 '21 at 19:54
  • 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. – Simon Jun 18 '21 at 20:02
  • Here is my actual code: https://www.dropbox.com/s/qn0dan0m61593gd/1.png?dl=0 https://www.dropbox.com/s/cfvlmsei3gdyxkl/2.png?dl=0 – Simon Jun 18 '21 at 20:06
  • @Simon Seems you could just use a fallback value or Nullish Coalescing to handle this when you are consuming the previous servings value, i.e. `prevServings || originalServings` or `prevServings ?? originalServings`. I guess it would be `originalData.servings` from your snippet (*it's a little unclear where the the servings data actually originates*)? Even something like `const prevServings = usePrevious(duplicate.servings) ?? originalData.servings;` may work. – Drew Reese Jun 18 '21 at 20:17
  • The data is actually fetched and used in a parent component and passed to this child component https://www.dropbox.com/s/bqxva1o1t02yq9k/3.png?dl=0 – Simon Jun 18 '21 at 20:21
  • I tried all the methods you mentioned above it does return the default value on first render but the value is returned twice like first render: 8, second render: 8, third render: 9. – Simon Jun 18 '21 at 20:29
  • @Simon Wouldn't that be expected since you are providing fallback for the `undefined` value from the non-existent previous render on initial render and caching the current value from the current render for the *next* render cycle? Maybe you are just caching the wrong value then? Sounds like you are really wanting to cache some computed value. Maybe you just aren't using the component lifecycle correctly? There's no possible way to get a previous value from a render cycle that never happened. Can you provide a *running* codesandbox that reproduces your issue that we can inspect and live debug? – Drew Reese Jun 18 '21 at 20:37
  • Here is the link: https://codesandbox.io/s/n34cb?file=/src/pages/DetailedRecipe.js:152-209 But for some reason, the Navbar component keeps showing errors. The child component is IngredientMeasurement.js and the parent one is DetailedRecipe.js (pages folder). I really appreciate your help so far! – Simon Jun 18 '21 at 21:01
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/233946/discussion-between-drew-reese-and-simon). – Drew Reese Jun 18 '21 at 22:24