How is getSnapshotBeforeUpdate implemented with Hooks?
Tags: featuredposts, react, javascript
Published: 2019-11-21 14:19:26

---
title: How is getSnapshotBeforeUpdate implemented with Hooks?
published: true
date: 2019-11-20 18:47:02 UTC
tags: Featuredposts,react,javascript
canonical_url: https://blog.logrocket.com/how-is-getsnapshotbeforeupdate-implemented-with-hooks/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/xnybi3tlbg4ulzs2izlj.png
---
**Written by [Ohans Emmanuel](https://blog.logrocket.com/author/ohansemmanuel/)**✏️

TL;DR: The short answer is `getSnapshotBeforeUpdate` can’t be implemented with Hooks. However, the more interesting question is why not? And what can we learn from implementing this ourselves?

## Introduction

It’s been over a year since the introduction of [Hooks](https://medium.com/free-code-camp/learn-the-basics-of-react-hooks-in-10-minutes-b2898287fe5d) and it’s no surprise they have been widely adopted by the React community. The introduction of React Hooks inspired other libraries [such as Vue](https://twitter.com/youyuxi/status/1137567675356291072?lang=en), to also create a function-based component API . One year later, it is fair to say the frontend community has largely embraced the functional approach to building components promoted by Hooks.

For the curious mind, you must have at some point asked if Hooks cover all use cases React classes handled. Well, [the answer is no](https://reactjs.org/docs/hooks-faq.html#do-hooks-cover-all-use-cases-for-classes). There are no Hook equivalents for the `getSnapshotBeforeUpdate` and `componentDidCatch` class lifecycle methods. To be fair, these methods aren’t used as much as the others – regardless they are still helpful lifecycle methods and the React team had promised to release this “soon”.

With that being said, could we at least try to implement the `getSnapshotBeforeUpdate` lifecycle method with Hooks? If it were possible within the confines of the Hooks available to us now, what would be our best shot at implementing this?

In the following section, we’ll try to implement the `getSnapshotBeforeUpdate` using `useLayoutEffect` and `useEffect`.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## The demo app

To make this as pragmatic as possible we’ll work with the following [demo app](https://github.com/ohansemmanuel/points):

![example app](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/exampleappusesnapshot.gif?resize=730%2C439&ssl=1)

This app has a pretty simple setup. The app renders a football and scored points on the left, but more importantly, it also renders a chat pane to the right. What’s important about this chat pane is that as more chat messages are rendered in the pane (by clicking the _add chat_ button), the pane is automatically scrolled down to the latest message i.e auto-scroll. This is a common requirement for chat apps such as WhatsApp, Skype, iMessage. As you send more messages the pane auto scrolls so you don’t have to do so manually.

I explain how this works in a previous [write up](https://blog.logrocket.com/the-new-react-lifecycle-methods-in-plain-approachable-language-61a2105859f3/) on lifecycle methods, but I’m happy to do a simple recap.

## Recap: How getSnapshotBeforeUpdate works for auto-scroll

In a nutshell, you check if there are new chat messages and return the dimension to be scrolled within the `getSnapshotBeforeUpdate` lifecycle method as shown below:

```jsx
getSnapshotBeforeUpdate (prevProps, prevState) {
    if (this.state.chatList > prevState.chatList) {
      const chatThreadRef = this.chatThreadRef.current
      return chatThreadRef.scrollHeight - chatThreadRef.scrollTop
    }
    return null
  }
```

Here’s how the code snippet above works.

First, consider a situation where the entire height of all chat messages doesn’t exceed the height of the chat pane.

![chat message scroll height](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/chatmessages.png?resize=1104%2C708&ssl=1)

Here, the expression `chatThreadRef.scrollHeight - chatThreadRef.scrollTop` will be equivalent to `chatThreadRef.scrollHeight - 0`.

When this is evaluated, the returned value from `getSnapshotBeforeUpdate` will be equal to the `scrollHeight` of the chat pane — just before the new message is inserted to the DOM.

If you remember how `getSnapshotBeforeUpdate` works, the value returned from the `getSnapshotBeforeUpdate` method is passed as the third argument to the `componentDidUpdate` method.

We call this value, `snapshot`:

```jsx
componentDidUpdate(prevProps, prevState, snapshot) {

}
```

The snapshot value passed in here — at this time, is the previous `scrollHeight` before the DOM is updated.

In the `componentDidUpdate` lifecycle method, here’s the code that updates the scroll position of the chat pane:

```jsx
componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot !== null) {
      const chatThreadRef = this.chatThreadRef.current;
      chatThreadRef.scrollTop = chatThreadRef.scrollHeight - snapshot;
    }
  }
```

In actuality, we are programmatically scrolling the pane vertically from the top down, by a distance equal to `chatThreadRef.scrollHeight - snapshot`.

Since snapshot refers to the `scrollHeight` _before_ the update, the above expression returns the height of the new chat message plus any other related height owing to the update. Please see the graphic below:

![scroll height before update](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/scrollHeightbeforeupdate.png?resize=1024%2C657&ssl=1)

When the entire chat pane height is occupied with messages (and already scrolled up a bit), the snapshot value returned by the `getSnapshotBeforeUpdate` method will be equal to the actual height of the chat pane.

![chat pane height](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/chatpaneheight.png?resize=1104%2C708&ssl=1)

The computation from `componentDidUpdate` will set the `scrollTop` value to the sum of the heights of extra messages – exactly what we want.

And, that’s it!

## How do we replicate this with Hooks?

The goal here is to try as much as possible to recreate a similar API using Hooks. While this is not entirely possible, let’s give it a shot!

To implement `getSnapshotBeforeUpdate` with Hooks, we’ll write a custom Hook called `useGetSnapshotBeforeUpdate` and expect to be invoked with a function argument like this:

```jsx
useGetSnapshotBeforeUpdate(() => {

})
```

The class lifecycle method, `getSnapshotBeforeUpdate` gets called with `prevProps` and `prevState`. So we’d expect the function passed to `useGetSnapshotBeforeUpdate` to be invoked with the same arguments.

```jsx
useGetSnapshotBeforeUpdate((prevProps, prevState) => {

})
```

There’s simply no way to get access to `prevProps` and `prevState` except by writing a custom solution. One approach involves the user passing down the current `props` and `state` to the custom Hook, `useGetSnapshotBeforeUpdate`. The Hook will accept two more arguments, `props` and `state` – from these, we will keep track of `prevProps` and `prevState` within the Hook.

```jsx
useGetSnapshotBeforeUpdate((prevProps, prevState) => {

}, props, state)
```

Now let’s write the internals of the `useGetSnapshotBeforeUpdate` Hook by getting a hold of the previous `props` and `state`.

```jsx
// custom Hook for getting previous props and state
// https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
const usePrevPropsAndState = (props, state) => {
  const prevPropsAndStateRef = useRef({ props: null, state: null })
  const prevProps = prevPropsAndStateRef.current.props
  const prevState = prevPropsAndStateRef.current.state

  useEffect(() => {
    prevPropsAndStateRef.current = { props, state }
  })

  return { prevProps, prevState }
}

// actual hook implementation 
const useGetSnapshotBeforeUpdate = (cb, props, state) => {
  // get prev props and state
  const { prevProps, prevState } = usePrevPropsAndState(props, state)

}
```

As seen above, the `useGetSnapshotBeforeUpdate` Hook takes the user callback, props, and state as arguments then invokes the `usePrevPropsAndState` custom Hook to get a hold of the previous props and state.

Next, it is important to understand that the class lifecycle method, `getSnapshotBeforeUpdate` is never called on mount. It is only invoked when the component updates. However, the Hooks `useEffect` and `useLayoutEffect` are by default, always called at least once on mount. We need to prevent this from happening.

Here’s how:

```jsx
const useGetSnapshotBeforeUpdate = (cb, props, state) => {
  // get prev props and state
  const { prevProps, prevState } = usePrevPropsAndState(props, state)

        
// getSnapshotBeforeUpdate - not run on mount + run on every update
  const componentJustMounted = useRef(true)
  useLayoutEffect(() => {
    if (!componentJustMounted.current) {
      // do something
    }
    componentJustMounted.current = false
  })
}
```

To prevent `useLayoutEffect` from running on mount we keep hold of a ref value `componentJustMounted` which is true by default and only set to false at least once after `useLayoutEffect` is already fired.

If you paid attention, you’d notice I used the `useLayoutEffect` Hook and not `useEffect`. Does this matter?

Well, there’s a reason why I did this.

The class lifecycle method `getSnapshotBeforeUpdate` returns a snapshot value that is passed on to the `componentDidUpdate` method. However, this snapshot is usually value retrieved from the DOM before React has had the chance to commit the changes to the DOM.

Since `useLayoutEffect` is always fired before `useEffect`, it is the closest we can get to retrieving a value from the DOM before the browser has had the chance to paint the changes to the screen.

Also, note that the `useLayoutEffect` Hook is NOT called with any array dependencies – this makes sure it fires on every update/re-render.

Let’s go ahead and get the snapshot. Note that this is the value returned from invoking the user’s callback.

```jsx
const useGetSnapshotBeforeUpdate = (cb, props, state) => {
  // get prev props and state
  const { prevProps, prevState } = usePrevPropsAndState(props, state)
  // 👇 look here
  const snapshot = useRef(null)


// getSnapshotBeforeUpdate - not run on mount + run on every update
  const componentJustMounted = useRef(true)
  useLayoutEffect(() => {
    if (!componentJustMounted.current) {
      // 👇 look here
           snapshot.current = cb(prevProps, prevState)  
    }
    componentJustMounted.current = false
  })
}
```

So far, so good.

The concluding part of this solution involves accommodating for `componentdidUpdate` since it is closely used with `getSnapshotBeforeUpdate`.

Remember, the `componentdidUpdate` lifecycle method is invoked with `prevProps`, `prevState`, and the snapshot returned from `getSnapshotBeforeUpdate`.

To mimic this API we will have the user call a custom `useComponentDidUpdate` Hook with a callback:

```jsx
useComponentDidUpdate((prevProps, prevState, snapshot) => {

})
```

How do we do this? One solution is to return the `useComponentDidUpdate` Hook from the `useGetSnapshotBeforeUpdate` Hook earlier built. Yes, a custom Hook can return another! By doing this we take advantage of JavaScript [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures).

Here’s the implementation of that:

```jsx
const useGetSnapshotBeforeUpdate = (cb, props, state) => {
  // get prev props and state
  const { prevProps, prevState } = usePrevPropsAndState(props, state)

  const snapshot = useRef(null)


// getSnapshotBeforeUpdate - not run on mount + run on every update
  const componentJustMounted = useRef(true)
  useLayoutEffect(() => {
    if (!componentJustMounted.current) {
           snapshot.current = cb(prevProps, prevState)  
    }
    componentJustMounted.current = false
  })

 // 👇 look here
 const useComponentDidUpdate = cb => {
    useEffect(() => {
      if (!componentJustMounted.current) {
        cb(prevProps, prevState, snapshot.current)
      }
    })
  }
  // 👇 look here
  return useComponentDidUpdate
}
```

There are a couple things to note from the code block above. First, we also prevent the user callback from being triggered when the component just mounts — since `componentDidUpdate` isn’t invoked on mount.

Also, we use the `useEffect` Hook here and not `useLayoutEffect`.

And that is it! We’ve made an attempt to reproduce the APIs for `getSnapshotBeforeUpdate`, but does this work?

## Testing out the implemented solution

We may now refactor the App component from the demo to use [Hooks](https://github.com/ohansemmanuel/points/blob/f9206c709f475d19b414fc689e357edb70c34696/src/App2.js). This includes using the custom Hooks we just built like this:

```jsx
const App = props => {
  // other stuff ...

  const useComponentDidUpdate = useGetSnapshotBeforeUpdate(
    (_, prevState) => {
      if (state.chatList > prevState.chatList) {
        return (
          chatThreadRef.current.scrollHeight - chatThreadRef.current.scrollTop
        )
      }
      return null
    },
    props,
    state
  )

  useComponentDidUpdate((prevProps, prevState, snapshot) => {
    console.log({ snapshot }) // 👈 look here
    if (snapshot !== null) {
      chatThreadRef.current.scrollTop =
        chatThreadRef.current.scrollHeight - snapshot
    }
  })
}
```

The implementation within these Hooks is just the same as the class component. However, note that I’ve logged the snapshot received from our custom implementation.

From the implementation with class lifecycle methods here’s what you get:

![scrollTop](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/scrollToplog.png?resize=730%2C376&ssl=1)

The snapshot is indeed received before the React commits to the DOM. We know this because the snapshot refers to the `scrollHeight` before the actual update and in the screenshot, it is obviously different from the current `scrollHeight`.

However with our Hooks implementation, the previous `scrollHeight` which is, in fact, the snapshot we seek, is never different from the current `scrollHeight`.

![snapshot and scrollTop](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/snapshotscrollTop.png?resize=730%2C378&ssl=1)

 

For some reason, we’re unable to catch the snapshot before the DOM is updated. Why’s this the case?

## Conclusion

While it may seem insignificant, this exercise is great for questioning your understanding of Hooks and certain React fundamentals. In a nutshell, we’re unable to get a hold of the snapshot before the DOM is updated because all Hooks are invoked in the React “commit phase” – after React has updated the DOM and refs internally.

![lifecycle methods diagram](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2019/11/commitphase.png?resize=1221%2C678&ssl=1)<figcaption id="caption-attachment-10166">http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/</figcaption>

Since `getSnapshotBeforeUpdate` is invoked before the commit phase this makes it impossible to be replicated within the confines of just the Hooks, `useEffect` and `useLayoutEffect`.

I hope you enjoyed the discourse and learned something new. Stay [up to date](https://twitter.com/OhansEmmanuel) with my writings.

* * * 
**Editor's note:** Seeing something wrong with this post? You can find the correct version [here](https://blog.logrocket.com/how-is-getsnapshotbeforeupdate-implemented-with-hooks/)).

## Plug: [LogRocket](https://logrocket.com/signup/), a DVR for web apps
 
![LogRocket Dashboard Free Trial Banner](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png?resize=1200%2C677&ssl=1)
 
[LogRocket](https://logrocket.com/signup/) is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
[Try it for free](https://logrocket.com/signup/).
* * *
The post [How is getSnapshotBeforeUpdate implemented with Hooks?](https://blog.logrocket.com/how-is-getsnapshotbeforeupdate-implemented-with-hooks/) appeared first on [LogRocket Blog](https://blog.logrocket.com).