Skip to content

Instantly share code, notes, and snippets.

@jaredwilli
Forked from fdidron/reactfire.js
Created November 28, 2017 14:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jaredwilli/e8fd4a91ec5f43a802a7e5589a95e1fc to your computer and use it in GitHub Desktop.
Save jaredwilli/e8fd4a91ec5f43a802a7e5589a95e1fc to your computer and use it in GitHub Desktop.
ReactFire ES6 HOC proposal
import React from 'react';
/*************/
/* HELPERS */
/*************/
/**
* Returns the key of a Firebase snapshot across SDK versions.
*
* @param {DataSnapshot} snapshot A Firebase snapshot.
* @return {string|null} key The Firebase snapshot's key.
*/
function _getKey(snapshot) {
let key;
if (typeof snapshot.key === 'function') {
key = snapshot.key();
} else if (typeof snapshot.key === 'string' || snapshot.key === null) {
key = snapshot.key;
} else {
key = snapshot.name();
}
return key;
}
/**
* Returns the reference of a Firebase snapshot or reference across SDK versions.
*
* @param {DataSnapshot|DatabaseReference} snapshotOrRef A Firebase snapshot or reference.
* @return {DatabaseReference} ref The Firebase reference corresponding to the inputted snapshot
* or reference.
*/
function _getRef(snapshotOrRef) {
let ref;
if (typeof snapshotOrRef.ref === 'function') {
ref = snapshotOrRef.ref();
} else {
ref = snapshotOrRef.ref;
}
return ref;
}
/**
* Returns the index of the key in the list. If an item with the key is not in the list, -1 is
* returned.
*
* @param {Array<any>} list A list of items.
* @param {string} key The key for which to search.
* @return {number} The index of the item which has the provided key or -1 if no items have the
* provided key.
*/
function _indexForKey(list, key) {
for (let i = 0, length = list.length; i < length; ++i) {
if (list[i]['.key'] === key) {
return i;
}
}
/* istanbul ignore next */
return -1;
}
/**
* Throws a formatted error message.
*
* @param {string} message The error message to throw.
*/
function _throwError(message) {
throw new Error('ReactFire: ' + message);
}
/**
* Validates the name of the variable which is being bound.
*
* @param {string} bindVar The variable which is being bound.
*/
function _validateBindVar(bindVar) {
let errorMessage;
if (typeof bindVar !== 'string') {
errorMessage = 'Bind variable must be a string. Got: ' + bindVar;
} else if (bindVar.length === 0) {
errorMessage = 'Bind variable must be a non-empty string. Got: ""';
} else if (bindVar.length > 768) {
// Firebase can only stored child paths up to 768 characters
errorMessage = 'Bind variable is too long to be stored in Firebase. Got: ' + bindVar;
} else if (/[\[\].#$\/\u0000-\u001F\u007F]/.test(bindVar)) {
// Firebase does not allow node keys to contain the following characters
errorMessage = 'Bind variable cannot contain any of the following characters: . # $ ] [ /. Got: ' + bindVar;
}
if (typeof errorMessage !== 'undefined') {
_throwError(errorMessage);
}
}
/**
* Creates a new record given a key-value pair.
*
* @param {string} key The new record's key.
* @param {any} value The new record's value.
* @return {Object} The new record.
*/
function _createRecord(key, value) {
let record = {};
if (typeof value === 'object' && value !== null) {
record = value;
} else {
record['.value'] = value;
}
record['.key'] = key;
return record;
}
/******************************/
/* BIND AS OBJECT LISTENERS */
/******************************/
/**
* 'value' listener which updates the value of the bound state variable.
*
* @param {string} bindVar The state variable to which the data is being bound.
* @param {Firebase.DataSnapshot} snapshot A snapshot of the data being bound.
*/
function _objectValue(bindVar, snapshot) {
let key = _getKey(snapshot);
let value = snapshot.val();
this.data[bindVar] = _createRecord(key, value);
this.setState(this.data);
}
/*****************************/
/* BIND AS ARRAY LISTENERS */
/*****************************/
/**
* 'child_added' listener which adds a new record to the bound array.
*
* @param {string} bindVar The state variable to which the data is being bound.
* @param {Firebase.DataSnapshot} snapshot A snapshot of the data being bound.
* @param {string|null} previousChildKey The key of the child after which the provided snapshot
* is positioned; null if the provided snapshot is in the first position.
*/
function _arrayChildAdded(bindVar, snapshot, previousChildKey) {
let key = _getKey(snapshot);
let value = snapshot.val();
let array = this.data[bindVar];
// Determine where to insert the new record
let insertionIndex;
if (previousChildKey === null) {
insertionIndex = 0;
} else {
let previousChildIndex = _indexForKey(array, previousChildKey);
insertionIndex = previousChildIndex + 1;
}
// Add the new record to the array
array.splice(insertionIndex, 0, _createRecord(key, value));
// Update state
this.setState(this.data);
}
/**
* 'child_removed' listener which removes a record from the bound array.
*
* @param {string} bindVar The state variable to which the data is bound.
* @param {Firebase.DataSnapshot} snapshot A snapshot of the bound data.
*/
function _arrayChildRemoved(bindVar, snapshot) {
let array = this.data[bindVar];
// Look up the record's index in the array
let index = _indexForKey(array, _getKey(snapshot));
// Splice out the record from the array
array.splice(index, 1);
// Update state
this.setState(this.data);
}
/**
* 'child_changed' listener which updates a record's value in the bound array.
*
* @param {string} bindVar The state variable to which the data is bound.
* @param {Firebase.DataSnapshot} snapshot A snapshot of the data to bind.
*/
function _arrayChildChanged(bindVar, snapshot) {
let key = _getKey(snapshot);
let value = snapshot.val();
let array = this.data[bindVar];
// Look up the record's index in the array
let index = _indexForKey(array, key);
// Update the record's value in the array
array[index] = _createRecord(key, value);
// Update state
this.setState(this.data);
}
/**
* 'child_moved' listener which updates a record's position in the bound array.
*
* @param {string} bindVar The state variable to which the data is bound.
* @param {Firebase.DataSnapshot} snapshot A snapshot of the bound data.
* @param {string|null} previousChildKey The key of the child after which the provided snapshot
* is positioned; null if the provided snapshot is in the first position.
*/
function _arrayChildMoved(bindVar, snapshot, previousChildKey) {
let key = _getKey(snapshot);
let array = this.data[bindVar];
// Look up the record's index in the array
let currentIndex = _indexForKey(array, key);
// Splice out the record from the array
let record = array.splice(currentIndex, 1)[0];
// Determine where to re-insert the record
let insertionIndex;
if (previousChildKey === null) {
insertionIndex = 0;
} else {
var previousChildIndex = _indexForKey(array, previousChildKey);
insertionIndex = previousChildIndex + 1;
}
// Re-insert the record into the array
array.splice(insertionIndex, 0, record);
// Update state
this.setState(this.data);
}
/*************/
/* BINDING */
/*************/
/**
* Creates a binding between Firebase and the inputted bind variable as either an array or
* an object.
*
* @param {Firebase} firebaseRef The Firebase ref whose data to bind.
* @param {string} bindVar The state variable to which to bind the data.
* @param {function} cancelCallback The Firebase reference's cancel callback.
* @param {boolean} bindAsArray Whether or not to bind as an array or object.
*/
function _bind(firebaseRef, bindVar, cancelCallback, bindAsArray) {
if (Object.prototype.toString.call(firebaseRef) !== '[object Object]') {
_throwError('Invalid Firebase reference');
}
_validateBindVar(bindVar);
if (typeof this.firebaseRefs[bindVar] !== 'undefined') {
if(module.hot) {
this.unbind(bindVar);
}
else{
_throwError('this.state.' + bindVar + ' is already bound to a Firebase reference');
}
}
// Keep track of the Firebase reference we are setting up listeners on
this.firebaseRefs[bindVar] = _getRef(firebaseRef);
if (bindAsArray) {
// Set initial state to an empty array
this.data[bindVar] = [];
this.setState(this.data);
// Add listeners for all 'child_*' events
this.firebaseListeners[bindVar] = {
child_added: firebaseRef.on('child_added', _arrayChildAdded.bind(this, bindVar), cancelCallback),
child_removed: firebaseRef.on('child_removed', _arrayChildRemoved.bind(this, bindVar), cancelCallback),
child_changed: firebaseRef.on('child_changed', _arrayChildChanged.bind(this, bindVar), cancelCallback),
child_moved: firebaseRef.on('child_moved', _arrayChildMoved.bind(this, bindVar), cancelCallback)
};
} else {
// Add listener for 'value' event
this.firebaseListeners[bindVar] = {
value: firebaseRef.on('value', _objectValue.bind(this, bindVar), cancelCallback)
};
}
}
export default (WrappedComponent) => class extends React.Component {
/********************/
/* HOC LIFETIME */
/********************/
/**
* Initializes the Firebase refs and listeners arrays.
**/
componentWillMount() {
this.data = {};
this.firebaseRefs = {};
this.firebaseListeners = {};
}
/**
* Unbinds any remaining Firebase listeners.
*/
componentWillUnmount() {
for (var bindVar in this.firebaseRefs) {
/* istanbul ignore else */
if (this.firebaseRefs.hasOwnProperty(bindVar)) {
this.unbind(bindVar);
}
}
}
render() {
return <WrappedComponent
{...this.state}
{...this.props}
bindAsArray={this.bindAsArray}
bindAsObject={this.bindAsObject}
unbind={this.unbind}
/>;
}
/*************/
/* BINDING */
/*************/
/**
* Creates a binding between Firebase and the inputted bind variable as an array.
*
* @param {Firebase} firebaseRef The Firebase ref whose data to bind.
* @param {string} bindVar The state variable to which to bind the data.
* @param {function} cancelCallback The Firebase reference's cancel callback.
*/
bindAsArray = (firebaseRef, bindVar, cancelCallback) => {
var bindPartial = _bind.bind(this);
bindPartial(firebaseRef, bindVar, cancelCallback, /* bindAsArray */ true);
}
/**
* Creates a binding between Firebase and the inputted bind variable as an object.
*
* @param {Firebase} firebaseRef The Firebase ref whose data to bind.
* @param {string} bindVar The state variable to which to bind the data.
* @param {function} cancelCallback The Firebase reference's cancel callback.
*/
bindAsObject = (firebaseRef, bindVar, cancelCallback) => {
var bindPartial = _bind.bind(this);
bindPartial(firebaseRef, bindVar, cancelCallback, /* bindAsArray */ false);
}
/**
* Removes the binding between Firebase and the inputted bind variable.
*
* @param {string} bindVar The state variable to which the data is bound.
* @param {function} callback Called when the data is unbound and the state has been updated.
*/
unbind = (bindVar, callback) => {
_validateBindVar(bindVar);
if (typeof this.firebaseRefs[bindVar] === 'undefined') {
_throwError('this.state.' + bindVar + ' is not bound to a Firebase reference');
}
// Turn off all Firebase listeners
for (var event in this.firebaseListeners[bindVar]) {
/* istanbul ignore else */
if (this.firebaseListeners[bindVar].hasOwnProperty(event)) {
var offListener = this.firebaseListeners[bindVar][event];
this.firebaseRefs[bindVar].off(event, offListener);
}
}
delete this.firebaseRefs[bindVar];
delete this.firebaseListeners[bindVar];
// Update state
var newState = {};
newState[bindVar] = undefined;
this.setState(newState, callback);
}
}
import React from 'react';
import ReactFire from './reactfire';
class UsageExample extends React.Component {
constructor(props) {
super(props)
}
componentWillMount() {
var ref = firebase.database().ref("items");
//ReactFire methods are passed as props
this.props.bindAsArray(ref, "items")
}
render() {
//Firebase binded data is in the props as well
console.log(this.props.items);
return (
<div>
<p>This is not relevant</p>
</div>
)
}
}
export default ReactFire(UsageExample);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment