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?
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
It’s been over a year since the introduction of Hooks 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, 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. 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.
To make this as pragmatic as possible we’ll work with the following demo app:

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 on lifecycle methods, but I’m happy to do a simple recap.
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:
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.

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:
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:
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:

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.

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!
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:
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.
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.
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.
// 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:
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.
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:
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.
Here’s the implementation of that:
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?
We may now refactor the App component from the demo to use Hooks. This includes using the custom Hooks we just built like this:
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:

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.

For some reason, we’re unable to catch the snapshot before the DOM is updated. Why’s this the case?
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.

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 with my writings.
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 29th issue.

Learn about the new features in the Next.js 16 release: why they matter, how they impact your workflow, and how to start using them.

Test out Meta’s AI model, Llama, on a real CRUD frontend projects, compare it with competing models, and walk through the setup process.

Rosario De Chiara discusses why small language models (SLMs) may outperform giants in specific real-world AI systems.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
2 Replies to "How is getSnapshotBeforeUpdate implemented with Hooks?"
Hey, nice article! I noticed that `useLayoutEffect` did not work for you as it really happens right *after* DOM rendering. The regular `useEffect` is even more delayed — it happens in the next animation frame after rendering.
What you could do was to make your snapshot right in the component function body. This is what actually gets called before DOM manipulations. You already have your new state/props and you can use refs to get previous ones.
This article, however nicely marked-up it is, is actually useless because `useLayoutEffect` is called right *after* DOM rendering, not before it.