Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Last active November 9, 2018 17:41
Show Gist options
  • Save ryanflorence/c379f7250e810b839d5c2080c39c2724 to your computer and use it in GitHub Desktop.
Save ryanflorence/c379f7250e810b839d5c2080c39c2724 to your computer and use it in GitHub Desktop.
import firebase from 'firebase/app'
import 'firebase/database'
const VERSION = 'v1'
const config = {
apiKey: '',
authDomain: '',
databaseURL: '',
storageBucket: '',
messagingSenderId: ''
}
firebase.initializeApp(config)
export const db = firebase.database()
export const getPath = (path) => `${VERSION}/${path}`
export const getRef = (path) => db.ref(getPath(path))
export const listToArray = (obj) => (
Object.keys(obj || {}).map((key) => (
{ ...obj[key], key }
))
)
import React, { PropTypes, Component } from 'react'
import { getRef, listToArray } from '../utils/firebase'
import { ErrorMessage, Loading } from '../Theme'
/*
```js
<Ref path="/somewhere">
{({ error, loaded, value }) => (
// `value` is an object w/ keys
// `error` is an error thrown by firebase
// `loaded` is whether or not the value has loaded at least once
// (can't just check `value === null` value can be null!)
)}
</Ref>
<Ref list={true} path="/somewhere">
{({ value }) => (
// `value` - will be an array with an added `key` property on each item
// when you add the `list={true}` prop
)}
</Ref>
<Ref valueOnly={true} path="/somewhere">
{(value) => (
// now instead of an object of all states (error, loaded, value)
// it only calls back when it's loaded so you can skip loaded checks,
// it'll render <Loading/> and <ErrorMessage/> for you
)}
</Ref>
<Ref
path="/somewhere"
query={(ref) => ref.limitToLast(50)}
/>
```
*/
class Ref extends Component {
static propTypes = {
path: PropTypes.string,
list: PropTypes.bool,
query: PropTypes.func,
children: PropTypes.func,
valueOnly: PropTypes.bool
}
static defaultProps = {
list: false,
query: (ref) => ref,
valueOnly: false
}
state = {
loaded: false,
value: null,
error: null
}
unmounted = false
componentDidMount() {
this.subscribe()
}
subscribe(props) {
props = props || this.props
const { path, list, query } = props
this.ref = query(getRef(path))
this.ref.on('value', (snapshot) => {
if (!this.unmounted) {
const value = snapshot.val()
this.setState({
value: list ? listToArray(value) : value,
loaded: true
})
}
}, (error) => {
if (!this.unmounted) {
this.setState({ error, loaded: true })
}
})
}
componentWillReceiveProps(nextProps) {
if (nextProps.path !== this.props.path) {
this.setState({ value: null, error: null, loaded: false })
this.unsubscribe()
this.subscribe(nextProps)
}
}
unsubscribe() {
this.ref.off()
}
componentWillUnmount() {
this.unsubscribe()
this.unmounted = true
}
render() {
const { children, valueOnly } = this.props
const { error, value, loaded } = this.state
return children ? (
valueOnly ? (
error ? (
<ErrorMessage error={error}/>
) : loaded ? (
children(value)
) : (
<Loading/>
)
) : (
children(this.state)
)
) : null
}
}
export default Ref
// SAMPLE USAGE
const addAccountToTransactions = (txs, accounts) => (
txs.map(tx => (
// add the relationship
{
...tx,
account: accounts[tx.accountKey]
}
))
)
const TransactionsWithAccounts = () => (
// these fetch in parallel
<Ref path="/accounts">
{(accounts) => (
<Ref path="/transactions" list={true}>
{(txs) => (
// txs will be an array
accounts.error || txs.error ? (
<ErrorMessages errors={[ accounts.error, txs.error ]}/>
) : accounts.loaded && txs.loaded ? (
children(addAccountToTransactions(transactions.value, accounts.value))
) : (
<Loading/>
)
)}
</Ref>
)}
</Ref>
)
// now it's super easy to just grab that data from anywhere, even w/ the foreign relationship!
const TransactionsDashboard = () => (
<Block>
<Header>Transactions</Header>
<TransactionsWithAccounts>
{(txs) => (
<Table txs={formatTransactions(txs)}/>
)}
</TransactionsWithAccounts>
</Block>
)
@jaredpalmer
Copy link

jaredpalmer commented Jan 25, 2017

This is pretty cool. I just tried this out. However, getting an invalid element error unless I children(value) with <span>:

// this works
const People = ({children}) => (
  <Ref list={true} path="/people">
    {({ error, loaded, value }) => {

      return (
        error ? (
          <ErrorMessage error={error} />
        ) : loaded ? (
          <span>{children(value)}</span>  // throws an error without <span> wrapping
        ) : (
          <Loading />
        )
    )}}
  </Ref>
)

export default const App = () => (
  <div>
    <People>
      {(ppl) => (
        ppl.map(p => <h2 key={p.key}>{p.firstName}</h2>)
      )}
    </People>
  <div>
)

@drKnoxy
Copy link

drKnoxy commented Feb 22, 2017

@ryanflorence awesome man, thanks for sharing. I think you can preserve the firebase order if you changed your listToArray

https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#forEach

function listToArray(snapshot) {
  const list = [];
  // snapshot implements forEach to obey order
  snapshot.forEach((v) => list.push({...v, key: v.key}) );
  return list;
}

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