Simon
Simon

Reputation: 47

Why usePrevious returns undefined on first render - react hooks?

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}/>
  )
}

Upvotes: 2

Views: 2406

Answers (3)

AlexGongadze
AlexGongadze

Reputation: 369

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

Upvotes: 0

Drew Reese
Drew Reese

Reputation: 202864

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"}

Upvotes: 0

Dan
Dan

Reputation: 10538

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.

Upvotes: 6

Related Questions