Firebase as a React Hook
source link: https://pragli.com/blog/firebase-as-a-react-hook/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
In a prior post, "How we use Firebase instead of React with Redux,"
I discussed how we created a withDbData
function to load data from Firebase Realtime Database (RTDB) into React conveniently.
Now that we've switched to writing most of our components as functions, I wanted a hook equivalent for loading state. In this post, I'll explain how to use and how I implemented useDbDatum / useDbData, two hooks for generically loading data from Firebase RTDB.
Note: you can get the code as a gist here .
Usage
useDbDatum
is a hook that loads a single datum at a single path in Firebase RTDB.
You could, for instance, use useDbDatum
as follows:
const Name = ({uid}) => { let name = useDbDatum(`users/${uid}/name`) return <div>{name}</div> }
Note that name
is null
initially, but the component rerenders with the value once it loads.
useDbData
loads multiple paths at the same time, returning an object where the keys are the paths and the values are the data in Firebase RTDB.
Most of the time, you'll want to use useDbDatum
over useDbData
- it's more convenient and direct - but I've had to use useDbData
once or twice in our code base. Most of the time, it has to do with performing ordering operations over lists, eg alphabetical sorting.
If ordering is not a requirement or a default ordering is good enough, I recommend making a subcomponent that uses useDbDatum
to load its own data, rather than loading the data in the list.
An example where we need useDbData
is the sorted student names class below. We load the list of student IDs from the class, using useDbDatum
, then load all of the students' names using useDbData
.
const SortedStudentNames = ({classUid}) => { let students = useDbDatum(`classes/${classUid}/students`); let uids = Object.keys(students || {}); let paths = studentIds.map(id => `students/${id}/name`); let nameValues = useDbData(paths); let names = Object.values(nameValues || {}); names.sort(); return <p>{names.join(', ')}</p> }
Implementation
During this implementation, I learned a lot about React hooks. I found it pretty quick to get up and running with useReducer
and useEffect
, but the tricky key to getting useDbData
working was useRef
.
useRef
provides an escape hatch from the other state of functional React components, which generally trigger rerenders when updated. If you're ever wondered how to replace this.something = {}
from React class components, useRef
may be your solution.
Doesn't that useRef
seem hacky? I thought so too, but I discovered that I wasn't the only one who used useRef
this way. Dan Abramov, one of the most famous contributors to React and author of Redux / create-react-app, also uses useRef
this way. Check out his blog post "Making setInterval Declarative with React Hooks"
for more.
Note: you can get the code as a gist here .
import React, { useReducer, useEffect, useRef } from 'react'; import firebase from 'firebase/app'; import equal from 'deep-equal'; function filterKeys(raw, allowed) { if (!raw) { return raw; } let s = new Set(allowed); return Object.keys(raw) .filter(key => s.has(key)) .reduce((obj, key) => { obj[key] = raw[key]; return obj; }, {}); } export const useDbData = (paths) => { let unsubscribes = useRef({}) let [data, dispatch] = useReducer((d, action) => { let {type, path, payload} = action switch (type) { case 'upsert': if (payload) { return Object.assign({}, d, {[path]: payload}) } else { let newData = Object.assign({}, d) delete newData[path] return newData } default: throw new Error('bad type to reducer', type) } }, {}) useEffect(() => { for (let path of Object.keys(paths)) { if (unsubscribes.current.hasOwnProperty(path)) { continue } let ref = firebase.database().ref(path) let lastVal = undefined let f = ref.on('value', snap => { let val = snap.val() val = paths[path] ? filterKeys(val, paths[path]) : val if (!equal(val, lastVal)) { dispatch({type: 'upsert', payload: val, path}) lastVal = val } }) unsubscribes.current[path] = () => ref.off('value', f) } let pathSet = new Set(Object.keys(paths)) for (let path of Object.keys(unsubscribes.current)) { if (!pathSet.has(path)) { unsubscribes.current[path]() delete unsubscribes.current[path] dispatch({type: 'upsert', path}) } } }) useEffect(() => { return () => { for (let unsubscribe of Object.values(unsubscribes.current)) { unsubscribe() } } }, []) return data } export const useDbDatum = (path, allowed=null) => { let datum = useDbData(path ? {[path]: allowed} : {}) if (datum[path]) { return datum[path] } return null }
Conclusion
Have any thoughts or questions about useDbData/Datum
? Let me know at [email protected] or on Twitter @dougsafreno
Learn More about Pragli
I'm the co-founder of Pragli, a virtual office for remote teams. Teams use Pragli to communicate faster and build closeness with one another. Learn morehere.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK