Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add connectFirestore to react-firebase #53

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import { Component, Children } from 'react'
export default class Provider extends Component {
static propTypes = {
firebaseApp: PropTypes.object.isRequired,
firestore: PropTypes.object,
children: PropTypes.element.isRequired,
}

static childContextTypes = {
firebaseApp: PropTypes.object,
firestore: PropTypes.object,
}

getChildContext() {
return {
firebaseApp: this.props.firebaseApp,
firestore: this.props.firestore,
}
}

Expand Down
185 changes: 185 additions & 0 deletions src/connect-firestore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import PropTypes from 'prop-types'
import { Component, createElement } from 'react'
import firebase from 'firebase/app'
import 'firebase/database'
import shallowEqual from 'shallowequal'
import { createQueryRef, getDisplayName, mapValues, pickBy, mapSnapshotToValue } from './utils'

const defaultMergeProps = (ownProps, firebaseProps) => ({
...ownProps,
...firebaseProps,
})

const mapSubscriptionsToQueries = subscriptions =>
mapValues(subscriptions, value => (typeof value === 'string' ? { path: value } : value))

const defaultMapFirebaseToProps = (props, ref, firestore) => ({
firestore,
})

export default (mapFirebaseToProps = defaultMapFirebaseToProps, mergeProps = defaultMergeProps) => {
const mapFirebase = (...args) => {
if (typeof mapFirebaseToProps !== 'function') {
return mapFirebaseToProps
}

const firebaseProps = mapFirebaseToProps(...args)

if (firebaseProps === null || typeof firebaseProps !== 'object') {
throw new Error(
`react-firebase: mapFirebaseToProps must return an object. Instead received ${firebaseProps}.`
)
}

return firebaseProps
}

const computeSubscriptions = (props, ref, firestore) => {
const firebaseProps = mapFirebase(props, ref, firestore)
return pickBy(firebaseProps, prop => typeof prop === 'string' || (prop && prop.path))
}

return WrappedComponent => {
class FirebaseConnect extends Component {
constructor(props, context) {
super(props, context)
this.firestore = props.firestore || context.firestore || firebase.firestore()

// polymorph based on number of /'s in path
this.ref = path =>
isCollection(path) ? this.firestore.collection(path) : this.firestore.doc(path)
this.state = {
subscriptionsState: null,
}
}

componentDidMount() {
const subscriptions = computeSubscriptions(this.props, this.ref, this.firestore)

this.mounted = true
this.subscribe(subscriptions)
}

componentWillReceiveProps(nextProps) {
const subscriptions = computeSubscriptions(this.props, this.ref, this.firestore)
const nextSubscriptions = computeSubscriptions(nextProps, this.ref, this.firestore)
const addedSubscriptions = pickBy(nextSubscriptions, (path, key) => !subscriptions[key])
const removedSubscriptions = pickBy(subscriptions, (path, key) => !nextSubscriptions[key])
const changedSubscriptions = pickBy(
nextSubscriptions,
(path, key) => subscriptions[key] && !shallowEqual(subscriptions[key], path)
)

this.unsubscribe({ ...removedSubscriptions, ...changedSubscriptions })
this.subscribe({ ...addedSubscriptions, ...changedSubscriptions })
}

componentWillUnmount() {
this.mounted = false

if (this.listeners) {
this.unsubscribe(this.listeners)
}
}

subscribe(subscriptions) {
if (Object.keys(subscriptions).length < 1) {
return
}

const queries = mapSubscriptionsToQueries(subscriptions)
const nextListeners = mapValues(queries, ({ path, ...query }, key) => {
const containsOrderBy = Object.keys(query).some(queryKey =>
queryKey.startsWith('orderBy')
)
const subscriptionRef = createQueryRef(this.ref(path), query)
const update = querySnapshot => {
if (this.mounted) {
if (isCollection(path)) {
// its a collection
const allstuff = []
querySnapshot.forEach(snapshot => {
const value = containsOrderBy ? mapSnapshotToValue(snapshot) : snapshot.data()
value._id = snapshot.id
allstuff.push(value)
})
this.setState(prevState => ({
subscriptionsState: {
...prevState.subscriptionsState,
[key]: allstuff,
},
}))
} else {
// its a document
const value = containsOrderBy
? mapSnapshotToValue(querySnapshot)
: querySnapshot.data()

this.setState(prevState => ({
subscriptionsState: {
...prevState.subscriptionsState,
[key]: value,
},
}))
}
}
}

const unsubscribe = subscriptionRef.onSnapshot(update)

return {
path,
unsubscribe: () => unsubscribe(),
}
})

this.listeners = { ...this.listeners, ...nextListeners }
}

unsubscribe(subscriptions) {
if (Object.keys(subscriptions).length < 1) {
return
}

const nextListeners = { ...this.listeners }
const nextSubscriptionsState = { ...this.state.subscriptionsState }

Object.keys(subscriptions).forEach(key => {
const subscription = this.listeners[key]
subscription.unsubscribe()

delete nextListeners[key]
delete nextSubscriptionsState[key]
})

this.listeners = nextListeners
this.setState({ subscriptionsState: nextSubscriptionsState })
}

render() {
const firebaseProps = mapFirebase(this.props, this.ref, this.firestore)
const actionProps = pickBy(firebaseProps, prop => typeof prop === 'function')
const subscriptionProps = this.state.subscriptionsState
const props = mergeProps(this.props, {
...actionProps,
...subscriptionProps,
})

return createElement(WrappedComponent, props)
}
}

FirebaseConnect.WrappedComponent = WrappedComponent
FirebaseConnect.defaultProps = Component.defaultProps
FirebaseConnect.displayName = `FirebaseConnect(${getDisplayName(WrappedComponent)})`
// FirebaseConnect.contextTypes = FirebaseConnect.propTypes = {
// firebaseApp: PropTypes.shape({
// database: PropTypes.func.isRequired // eslint-disable-line react/no-unused-prop-types
// })
// };

return FirebaseConnect
}
}

const isCollection = path => path.split('/').filter(x => x).length % 2
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export Provider from './Provider'
export connect from './connect'
export connectFirestore from './connect-firestore'