Skip to main content
How to get previous props/state with React Hooks

Share Everywhere

Accessing the previous props or state from within a functional component is one of those deceptively simple problems you’ll likely face as you work with React Hooks.

There’s currently no React Hook that does this out of the box, but you can manually retrieve either the previous state or props from within a functional component by leveraging the useRef hook.

How?

The solution is discussed in the official React documentation, and if you look in there you’ll find the following example, where Counter represents a simple counter component:

function Counter() {
  const [count, setCount] = useState(0);

  const prevCountRef = useRef();
  useEffect(() => {
    prevCountRef.current = count;
  });
  const prevCount = prevCountRef.current;

  return <h1>Now: {count}, before: {prevCount}</h1>;
}

If you’re looking for an even quicker solution, you may abstract this functionality Into the custom Hook below:

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

And use it within your application as follows:

function Counter() {
  const [count, setCount] = useState(0);
  //  look here
  const prevCount = usePrevious(count)

  return <h1> Now: {count}, before: {prevCount} </h1>;
}

Pretty simple, huh?

However, can you really explain how the usePrevious hook works?

I’ve shown how the problem is solved – a problem well-documented in the official docs. But the goal of this article isn’t to restate what’s been written in the official doc.

In the following section, I’ll explain in clear terms what’s happening within the custom Hook, and how the previous value is retained.

useRef: The unsung Hooks hero

I work with Hooks everyday, both for personal projects and professional workplace projects.

In my experience, I’ve found that very few people really understand and take advantage of the useRef hook.

Apart from being great at handling DOM refs, the useRef hook is a perfect substitute for implementing instance-like variables within functional components.

Consider the following example with class components:

// class component 
class Count extends Component {
  
   constructor() {
     this.specialVariable = "SPECIAL_VARIABLE"
   }
  
  render() {
    return null
  }
}

Every instantiated copy of the Count class will have its own specialVariable instance variable.

The useRef Hook can simulate this behavior, with an even more interesting feature.

// functional component 

function Count() {
  const specialVariable = useRef("SPECIAL_VARAIBLE");
  return null
}

What’s interesting about the useRef hook is that it takes in an initial value to be stored — i.e: useRef("INITIAL_VALUE") — and it returns an object with a current property {current: "INITIAL_VALUE"}.

Whatever value was initially passed into the useRef Hook is saved to the current property of the ref object.

function Count() {
  const specialVariable = useRef("SPECIAL_VARAIBLE");
  // specialVariable resolves to {current: "SPECIAL_VARIABLE"}

  return null
}

Unlike a ‘normal’ variable, the specialVariable ref object is not recomputed when the Count component is re-rendered. With the useRef Hook, the value saved in the ref object is kept the same across re-renders.

The value is not recomputed, nor is it lost. It remains the same.

Having said that, it’s worth mentioning that the only way to update the ref object is to directly set the value of the current property; e.g. specialVariable.current = "NEW_SPECIAL_VARIABLE.

Why is this explanation important?

Well, to understand what’s going on, let’s walk through the execution of the aforementioned solution to retrieving previous props/state step by step.

// custom hook for getting previous value 
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

// the App where the hook is used 
function Counter() {
  const [count, setCount] = useState(0);
  //  look here
  const prevCount = usePrevious(count)

  return <h1>Now: {count}, before: {prevCount}</h1>;
}

// How the App is rendered 
<Counter />

Now, as soon as the rendering process for the Counter app begins, what happens?

(1) The useState hook is invoked and the variables count and setCount set. Note that count is now 0.

The useState hook is invoked and the variables count and setCount set. Note that count is now 0.
The useState hook is invoked and the variables count and setCount set. Note that count is now 0.

After this, the next line is executed.

(2) the usePrevious Hook is invoked with the current value of the count state variable, 0.

Upon invoking the usePrevious Hook, the following happens:
Upon invoking the usePrevious Hook, the following happens:

Upon invoking the usePrevious Hook, the following happens:

(3) A new ref object is created.

const-ref-useref

This ref object is initialized without an initial value, so the object returned is this: {current: undefined}

This next step is where most people slip up.

(4) The useEffect call is NOT invoked. Instead, the return value of the custom Hook is invoked.

ref.current is returned, which in this case is undefined.
ref.current is returned, which in this case is undefined.

ref.current is returned, which in this case is undefined.

Why this behavior? i.e skipping the useEffect call?

Well, the useEffect hook is only invoked after the component from which it is called has been rendered (i.e, the return value of the component must be executed first.

So, what happens next?

(4) The execution within the component is resumed. This time, the prevCount variable holds the value undefined.

prevCount

(5) Next, the return value of the component is evaluated:

Next, the return value of the component is evaluated

This returns the following to the screen: <h1>Now: {count}, before: {prevCount}</h1>, where count  and prevCount are 0 and undefined.

(6) The useEffect call within the usePrevious hook is now invoked asynchronously in order to avoid blocking the browser from painting the DOM changes. useEffect is invoked after the render of the functional component.

Here’s what we have within the effect function:

Here’s what we have within the effect function:

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

The line within the useEffect function updates the current property of the ref object to value. What’s the value now?

value represents what the custom Hook was initially called with.

In this case, the value is 0. In this current flow, remember usePrevious has only been called once with the initial value of 0.

Here’s what we have within the effect function:

Now, the ref holds the value 0.

What happens when the count state variable within the app is updated from 0 to 1 (or a new count)?

The same flow is re-triggered.

The usePrevious Hook is invoked with the new state value 1. Then, the return statement is evaluated (return ref.current), which would be 0 — not 1 since the ref object isn’t updated yet.

ref.current here is the previous value stored before the useEffect was triggered, or 0.
The return statement of the component is equally evaluated with the previous value successfully returned.

Only after the render is the useEffect call within the usePrevious Hook updated with the new value, 1.

This cycle continues – and in this way, you’ll always get the previous value passed into the custom Hook, usePrevious.

Why this Works

To appreciate why this works this way, you must remember the following:

  1. The ref object will always return the same value held in ref.current, except when explicitly updated.
  2. useEffect is only called after the component is rendered with the previous value. Only after the render is done is the ref object updated within useEffect.

By taking advantage of these two facts, you can easily replicate this functionality on your own.

Bạn cảm thấy bài viết này như thế nào?

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
6 + 14 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.