import React 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; }, {}); } function parseSpec(spec) { let {template} = spec; let starts = template.split('{') starts.shift(); let propIds = []; for (let start of starts) { propIds.push(start.split('}')[0]); } let formatPath = (props) => { let vals = props; let path = template; for (let key of Object.keys(vals)) { if (!vals[key] && path.includes(`{${key}}`)) { return null; } path = path.replace(`{${key}}`, vals[key]); } return path; } return { propIds, formatPath }; } export function withDbData(specs) { let propToSpecs = {}; for (let spec of specs) { let {propIds} = parseSpec(spec); for (let propId of propIds) { if (!propToSpecs[propId]) { propToSpecs[propId] = []; } propToSpecs[propId].push(spec); } } return (Child) => { let Wrapper = class extends React.PureComponent { constructor(props) { super(props); this.unmounting = false; this.offs = {}; this.state = {}; } subscribeToSpec(spec) { let { name, keys } = spec; let { propIds, formatPath } = parseSpec(spec); let path = formatPath(this.props); if (!path) { return; } let ref = firebase.database().ref(path); let offFunc = ref.on('value', (snap) => { let dat = keys ? filterKeys(snap.val(), keys) : snap.val(); if (equal(dat, this.state[name])) { return; } this.setState({ [name]: dat, }); }); let hasBeenOffed = false; let off = () => { if (hasBeenOffed) { return; } hasBeenOffed = true; if (!this.unmounting) { this.setState({ [name]: null, }); } ref.off('value', offFunc); }; for (let propId of propIds) { if (!this.offs[propId]) { this.offs[propId] = []; } this.offs[propId].push(off) } } componentDidMount() { for (let spec of specs) { this.subscribeToSpec(spec) } } componentDidUpdate(prevProps) { let resubs = new Set(); for (let prop of Object.keys(propToSpecs)) { if (prevProps[prop] !== this.props[prop]) { if (this.offs[prop]) { for (let off of this.offs[prop]) { off(); } } this.offs[prop] = []; for (let spec of propToSpecs[prop]) { if (resubs.has(spec.name)) { continue; } resubs.add(spec.name); this.subscribeToSpec(spec); } } } } componentWillUnmount() { this.unmounting = true; for (let offList of Object.values(this.offs)) { for (let off of offList) { off(); } } this.offs = {}; } render() { for (let spec of specs) { if (spec.await && !this.state[spec.name]) { return null; } } let childProps = Object.assign({}, this.props, this.state); return (); } } return Wrapper; } }