React.js has, at it’s disposal, an army of hooks. These hooks not only streamline the development flow, they make the code cleaner and easy to understand. If you have ever worked in react you might know very well about these tiny life saving hooks. React provides a bunch of hooks, but the most important and widely used one is useEffect
It’s like the Swiss Army knife of React hooks, helping developers tackle all sorts of tasks. Seriously, it’s the go-to for many things. Now, don’t get me wrong, useEffect is fantastic—it’s like the rockstar of React hooks. But here’s the deal, because it’s so versatile, people often end up using it for everything, and that can lead to a bit of a mess.
One classic scenario where useEffect gets a bit misused is when folks try to use it for subscribing to external data stores. Sure, it seems like a good idea at first—useEffect is flexible, right? But here’s the catch: when you use it this way, your code can become a bit of a headache. It might start acting weird, causing memory leaks, and triggering re-renders when you least expect it. It’s like trying to fit a square peg into a round hole.
But then what to do? How do you subscribe to external data stores? Well React provides an out of the box hook for this useSyncExternalDatastore
. In this article useSyncExternalStore : A better alternative to useEffect
, we will discuss how to use useSyncExternalStore
for subscribing to external data stores.
What is an external data store?
Imagine you’re building an app with React, and you need a place to store all your data. That’s where an external data store
comes in. It’s like a super organized and secure storage room for your app’s data. You can put things in there and take them out whenever you need.
Now, let’s talk about a real-life example: the Firestore
. Think of it like a magical cloud storage for your app’s data. You can save stuff in there, and it updates in real-time. So, if you change something in one part of your app, it instantly changes in the storage too. It’s like having a smart and quick assistant organizing your data backstage while your app takes the spotlight. Firebase Realtime Database is just one of these cool storage options that React developers use to make their apps work smoothly.
useEffect : The bad way
Now let us get our hands dirty, let us write a sample component that uses useEffect
to subscribe to a node in firestore.
import React, { useState, useEffect } from 'react';
import firebase from 'firebase/app';
import 'firebase/firestore'; // Import Firestore
const FirebaseExample = () => {
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const db = firebase.firestore(); // Use Firestore instead of database
const collectionRef = db.collection('my-collection').doc('my-document'); // Use a collection instead of a node
// Problem 1: Multiple subscriptions on component re-renders
const unsubscribe = collectionRef.onSnapshot((snapshot) => {
const newData = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setData(newData);
});
// Problem 2: Unsubscribing only when the component unmounts
return () => {
unsubscribe(); // This will unsubscribe from the Firestore listener
};
};
fetchData();
}, []);
return (
<div>
<h2>Firebase Firestore Example</h2>
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
export default FirebaseExample;
As you can see, in the above code block, we are using useEffect to subscribe to an external datastore (firestore). Now let’s discuss about the potential problems in this approach.
- Multiple Subscriptions on Component Re-renders:
In the example, the collectionRef.onSnapshot is inside the useEffect, which means every time the component re-renders, a new subscription is created. Over time, if the component re-renders frequently (due to state changes or other reasons), you’ll end up with multiple subscriptions, leading to unexpected behavior and potential memory leaks. - Unsubscribing Only When the Component Unmounts:
The cleanup function (return () => {…}) attempts to unsubscribe from the Firestore. However, it’s not effective for handling the multiple subscriptions created during re-renders. Also, if the component unmounts while a subscription is active, it’s cleaned up, but if it unmounts and then mounts again quickly, you might lose data between unmount and remount.
These issues can make your code harder to maintain, prone to unexpected behaviors, and might affect the performance of your application. It’s where a more specialized solution, like the useSyncExternalStore
hook, comes in handy to streamline the process of syncing with external data stores in a cleaner way.
useSyncExternalStore: A cleaner way
This hook (useSyncExternalStore) is an out of the box solution provided by ReactJS to answer all the questions raised in the above paragraph.
const snapshot = useSyncExternalStore(subscribe, getSnapshot)
This is the general syntax of this hook. It takes in two functions as arguments and returns the current snapshot of the external datastore. Let us discuss about the 2 arguments.
subscribe
: A function that takes a singlecallback
argument and subscribes it to the store. When the store changes, it should invoke the providedcallback
. This will cause the component to re-render. Thesubscribe
function should return a function that cleans up the subscription.getSnapshot
: A function that returns a snapshot of the data in the store that’s needed by the component. While the store has not changed, repeated calls togetSnapshot
must return the same value. If the store changes and the returned value is different (as compared byObject.is
), React re-renders the component.
Let us see how we can use this hook to achieve the same functionality as in above example.
First of all let us create a file firebase.js
that encapsulates all the logic for interacting with firestore and exposes the functions for getting subscribing and getting the snapshot.
let data;
const firebaseStore = {
getSnapshot: () => {
return data;
},
subscribe: (callback) => {
const unsub = onSnapshot(doc(db, "my-collection", "my-document"), (doc) => {
data = doc.data()?.test;
callback();
});
return () => {
data = undefined;
unsub();
};
},
};
export default firebaseStore;
In this file, we expose two functions to subscribe to datastore and to get snapshot. Please notice that the subscribe
function takes a callback
function as an argument and calls it as and when the data changes. This helps the main component to re-render when the data changes.
Now let us use this in the main file.
const FirebaseExample = () => {
const data = useSyncExternalStore(firebaseStore.subscribe, firebaseStore.getSnapshot);
return (
<div>
<h2>Firebase Firestore Example</h2>
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
export default FirebaseExample;
And this is it, see how this hook makes the code look so simple and clean.
And it also eradicates the potential problems that were raised in the above approach. Since the subscription logic is no longer placed inside useEffect, it is not created every time the component is re rendered.
And this is why useSyncExternalStore : A better alternative to useEffect to subscribe to external datastore.
Please share this article if you found it helpful and for other articles on Javascript click here
0 Comments