Skip to content

Instantly share code, notes, and snippets.

@zomars
Forked from dsafreno/withDbData.js
Last active July 30, 2020 19:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zomars/0fa63604baf12517c5b7b592c1c72397 to your computer and use it in GitHub Desktop.
Save zomars/0fa63604baf12517c5b7b592c1c72397 to your computer and use it in GitHub Desktop.
Hook version of dsafreno's withDbData for RealTime Database subscribing in React.
import { useEffect, useRef, useState } from 'react';
import firebase from 'firebase/app';
import equal from 'deep-equal';
function filterKeys(raw, allowed) {
if (!raw) {
return raw;
}
const s = new Set(allowed);
return Object.keys(raw)
.filter(key => s.has(key))
.reduce((obj, key) => {
const newObj = obj;
newObj[key] = raw[key];
return newObj;
}, {});
}
export default function useRtDbData(specs = []) {
const isInitialMount = useRef(true);
const unmounting = useRef(false);
const offs = useRef({});
const [state, setState] = useState({});
const subscribeToSpec = spec => {
const { name, keys, path } = spec;
if (!path) {
return;
}
const ref = firebase.database().ref(path);
const offFunc = ref.on('value', snap => {
const dat = keys ? filterKeys(snap.val(), keys) : snap.val();
if (equal(dat, state[name])) {
return;
}
setState({
[name]: dat,
});
});
let hasBeenOffed = false;
const off = () => {
if (hasBeenOffed) {
return;
}
hasBeenOffed = true;
if (!unmounting.current) {
setState({
[name]: null,
});
}
ref.off('value', offFunc);
};
if (!offs.current[path]) {
offs.current[path] = null;
}
offs.current[path] = off;
};
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
specs.forEach(spec => {
subscribeToSpec(spec);
});
} else {
const resubs = new Set();
specs.forEach(spec => {
const { path } = spec;
if (typeof offs.current[path] === 'function') {
offs.current[path]();
}
offs.current[path] = null;
if (resubs.has(spec.name)) return;
resubs.add(spec.name);
subscribeToSpec(spec);
});
}
return () => {
unmounting.current = true;
Object.values(offs.current).forEach(offFunc => {
offFunc();
});
offs.current = {};
};
}, [specs]);
const isLoading = specs.some(spec => !state[spec.name]);
return { ...state, isLoading };
}
import React from 'react';
import useRtDbData from './useRtDbData';
const UserItemExample = () => {
const userId = 15;
const USERS_SPEC = {
name: 'user',
path: `users/${userId}`,
};
const { user, isLoading } = useRtDbData([USERS_SPEC]);
console.log('user', user);
if (isLoading) return null;
return <span>{user.name}</span>;
};
export default UserItemExample;
@zomars
Copy link
Author

zomars commented Jan 14, 2020

Updated so there's no need to define and parse a template since we can pass a dynamic path and the hook will update accordingly.

@zomars
Copy link
Author

zomars commented Jan 14, 2020

Removed await since the dev can decide wether to show or not a loading indicator if the data has not been loaded yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment