A Look At React Hooks: useSyncExternalStore
An awesome library Hook to integrate non-React state management in your apps!
Welcome to another article of A Look at React Hooks, a beginner-friendly series on React Hooks. In this article, let's learn about the useSyncExternalStore
Hook. It is a useful Hook for when you need to integrate non-React state management in your apps.
Before we begin, you should have a basic knowledge of React and how React Hooks work. If not, please read the React Hook series from the beginning.
What is useSyncExternalStore
useSyncExternalStore
a custom hook available in React 18, that lets you subscribe to an external store, reads updated values from that store and updates components if needed.
By 'external store', it means:
Third-party state management libraries that hold state outside of React
Browser APIs that expose a mutable value and events to subscribe to its changes
Some examples of 'external store' includes:
Browser history
localStorage
Third-party data sources
Why use useSyncExternalStore
According to the official docs, this Hook is useful when you want to update React components when some data in external stores changes. Without useSyncExternalStore
Hook, you would need to manually subscribe to these external stores using useEffect
.
In some cases, this might cause over-returning Hooks and unwanted re-rendering of components. Hence, it is more optimal to use useSyncExternalStore
. Note that the Hook itself doesn't inherently mitigate or address over-re-rendering issues caused by the use of useEffect
. Instead, it provides a way to synchronize your component with an external store in a more controlled manner, which may make your code less error-prone.
Here's an example: say you have a chat app like Discord with multiple text channels. You fetch the messagesData
from an external database like firestore.
You want the app to do 2 things:
When a new message is added to
messagesData
by the user, jump to the most recent message to display the newly added message in the windowWhen a new message is added to
messagesData
by another user to another text channel, display the newly added message when the user switches text channel
So, you might use useEffect
to achieve these 2 things like this:
const [messagesData, setMessagesData] = useState([]);
const [currentChannel, setCurrentChannel] = useState('general');
const messagesEndRef = useRef(null);
// useEffect to handle jumping to the most recent message when a new message is added
useEffect(() => {
// Assuming you have a function to fetch messages from Firestore
const fetchMessages = async () => {
const messages = await fetchMessagesFromFirestore(currentChannel);
setMessagesData(messages);
scrollToBottom();
};
fetchMessages();
// Subscribe to changes in Firestore for the current channel
const unsubscribe = subscribeToChannel(currentChannel, (newMessages) => {
setMessagesData(newMessages);
scrollToBottom();
});
return () => {
// Clean up the subscription when the component unmounts or the channel changes
unsubscribe();
};
}, [currentChannel]); // Re-run the effect when the current channel changes
// useEffect to handle displaying a new message when switching channels
useEffect(() => {
const unsubscribe = subscribeToChannel(currentChannel, (newMessages) => {
setMessagesData(newMessages);
scrollToBottom();
});
return () => {
unsubscribe();
};
}, [currentChannel]);
const scrollToBottom = () => {
// Scroll to the bottom of the messages container
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
};
const handleChannelChange = (newChannel) => {
setCurrentChannel(newChannel);
};
As seen from the example above, the useEffect
for fetching messages is triggered whenever the currentChannel
changes. While this is necessary for updating the messages for the new channel, it might lead to unnecessary re-renders if the component is re-rendered for reasons unrelated to the channel change.
Implementation
Here's a simple example of how to use the Hook:
const externalStore = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
The Hook returns a snapshot of the external store's data you are subscribed to.
subscribe
: Invokes callback function that triggers when component re-renders. It should also handle the cleanup of the subscription.getSnapshot
: This fetches and returns a snapshot of the data subscribed. If the returned value is different, then the component re-renders.getServerSnapshot
(optional): This function returns the initial snapshot of the data in the store and provides the snapshot during server-side rendering.
Minimalistic Example
Let's see how this Hook works in an example. Let's say you want to listen for changes in localStorage
.
First, you would import the Hook in your component.
import {useSyncExternalStore} from 'react'
Then, you would implement the Hook to read data from localStorage
like this:
//this is the subscribe function, listens for changes
const subscribe = (listener) => {
window.addEventListener("storage", listener);
return () => {
window.removeEventListener("storage", listener);
};
}
//this is the getSnapShot function, returns data subscribed
const getSnapShot = () => {
return localStorage.getItem("example");
}
//implement the Hook
const exampleValue = useSyncExternalStore(subscribe, getSnapShot);
Keep in mind this is a minimalistic example, but it should give you a gist on how it works. So the subscribe
function will listen for changes in localStorage
and handle unsubscribing as well. Then getSnapShot
gets the value of the data.
Finally, we use the useSyncExternalStore
to ensure the component re-renders whenever a change is detected from localStorage
, the external data store we are subscribing to.
For more examples, do visit the official website on useSyncExternalStore.
Another Example
How about we go back to the chat app example mentioned earlier? How can we use useSyncExternalStore
to simplify synchronization with our external database?
As shown in the example below, using this Hook allows a centralized approach to handle data from external sources and a more simplified code for real-time data updates.
const [currentChannel, setCurrentChannel] = useState('general');
const messagesEndRef = useRef(null);
// useSyncExternalStore for fetching messages
const messages = useSyncExternalStore(
() => subscribeToChannel(currentChannel, updateMessages),
() => fetchMessagesFromFirestore(currentChannel),
null // No server snapshot in this example, but you can add it if needed
);
useEffect(() => {
scrollToBottom();
}, [messages]);
const updateMessages = (newMessages) => {
// Handle the newly received messages
// This callback will be invoked whenever there's a change in the external store
};
const scrollToBottom = () => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
};
const handleChannelChange = (newChannel) => {
setCurrentChannel(newChannel);
};
Conclusion
In conclusion, the useSyncExternalStore
Hook has been a great and useful addition to React's kit. It would take time to learn some of the caveats of this Hook and fully understand how to implement it, so I hope this article is a good starting point for you.
Thanks for reading! Please share and like the article if it has been a helpful read. Also, do share in the comments your thoughts on this React Hook. Feel free to check out the References section below to read more. Till next time, cheers!