A real-time hook with Firebase & React-Query
In an app that I’ve recently worked on, I had to interact with 2 different APIs: a typical JSON-based API from a django project, as well as a “real-time API” which was coming directly from Firebase. That meant that, when reading data, I had to sometimes read them from Firebase and sometimes from another JSON API.
I wanted used react-query
for my remote data management needs, while also using the Firebase web client (as recommended) for all-things-firebase. While there’s nothing wrong with that, it felt a bit cumbersome to have two (2) different ways of querying and storing data. react-query
came with batteries included on the state management side, but unfortunately, the same can’t be said about the Firebase client, which not only serves as a simple transport client, but also has a completely different API.
What I wanted is to have a unified way of querying & storing my data, without having to differentiate my approach based on where the data comes from. In this article we’ll implement just that.
Although fully optional, the first thing I did was to create a wrapper client for firebase so that I could add typings (I was working on a TS project), centralize the Firebase error handling logic and add some syntactic sugar to create a more barebones/clean API for my fetching needs. A stripped-down example of this can be seen below:
While this wrapper (nicknamed RealTimeAPI
) doesn’t wrap all Firebase methods, it’s complete enough to handle all basic read operations that we’ll need. I want to stress that this is something I chose to do, but under no means was it mandatory.
With this implemented, we can go ahead and explore our options with react-query
. A thing to remember, is that react-query
is transport-agnostic; this means that it simply doesn’t care where and how data gets fetched, as long as a Promise gets resolved. With this in mind, we can go ahead and create a new useRealTimeQuery
hook which will be an alternative to the useQuery
hook that react-query
already exposes. This would look something like the following:
Some things to unpack about the hook above:
- It uses the firebase path key as the
react-query
’s query key since it’s guaranteed to be stable & unique - It creates an adapter interface between the callback-based land that firebase lives in and the Promise-based one that
react-query
likes. You’d think that the data Promise endlessly hangs and never resolves, butreact-query
resolves it instantly whenqueryClient.setQueryData
is called. - It automatically subscribes to updates and re-renders the React component when new data arrives
- It automatically unsubscribes if the React component gets unmounted thus not adding to your FB transport limits, nor unnecessarily stressing the main thread
- Although
isLoading
is initiallytrue
while waiting for our first batch of data, it will never betrue
again since updates will be coming in real-time.
With this, we are pretty much done. We can do:
const { data, isLoading, error } = useRealTimeQuery('/me/friends');
and it would work exactly how you’d expect it to.
As a final note, if you want to transform the data that comes in from firebase, react-query
supports data selectors which allow you to do just that.
Thanks for reading!
P.S. 👋 Hi, I’m Aggelos! If you liked this, consider following me on twitter or medium 😀