Skip to content

Instantly share code, notes, and snippets.

@marbemac
Last active August 29, 2015 14:17
Show Gist options
  • Save marbemac/a18582da8d2912285950 to your computer and use it in GitHub Desktop.
Save marbemac/a18582da8d2912285950 to your computer and use it in GitHub Desktop.
A query mixin for Baobab, supporting local and remote fetching, error handling, and loading handling.
// actions.js provides functions to fetch
// remote data and store it in the tree
// these remote functions should return
// promises
require('es6-promise').polyfill();
var stateTree = require('./stateTree'),
PostModel = require('./post'),
fetch = require('isomorphic-fetch'),
utils = require('./utils'),
_merge = require('lodash.merge'),
format = require('util').format,
debug = require('../mixins/debug');
/////////////////////
// MODEL CONSTANTS //
/////////////////////
var modelMeta = {
post: {
url: '/posts',
stateStore: 'posts',
instance: PostModel
}
}
var models = {
POST: 'post'
}
///////////////////
// QUERY METHODS //
///////////////////
var findOneQueries = {};
function findOne(model, id, force) {
// utils.buildQueryId creates a consistent, unique signature for this model/id
var queryId = utils.buildQueryId(model, id);
// If we're forcing a lookup, or we haven't looked this up before
if (force || !findOneQueries[queryId]) {
var r = get(format(modelMeta[model].url + '/%s', id), null)
var p = new Promise(function(resolve, reject) {
r.then(function(res) {
findOneQueries[queryId] = true;
if (!res.data) {
reject(res);
return;
}
setModel(model, res.data);
resolve(res.data);
}).catch(function(err) {
findOneQueries[queryId] = true;
debug.error('store:error', err);
reject(err);
});
});
findOneQueries[queryId] = p;
}
return findOneQueries[queryId];
}
var findQueries = {};
function find(model, query, modifiers, force) {
// utils.buildQueryId creates a consistent, unique signature for this model/params
var params = _merge(query, modifiers),
queryId = utils.buildQueryId(model, params);
// If we're forcing a lookup, or we haven't looked this up before
if (force || !findQueries[queryId]) {
var r = get(modelMeta[model].url, params);
var p = new Promise(function(resolve, reject) {
r.then(function(res) {
findQueries[queryId] = true;
if (!res.data) {
reject(res);
return;
}
// Loop through the response data and set/overwrite the models in the store
var storedData = stateTree.get(['models', modelMeta[model].stateStore]);
for (var k in res.data) {
storedData[res.data[k].id] = new modelMeta[model].instance(res.data[k]);
}
// update the tree, overwriting the entire models.[modelType] subtree
stateTree.set(['models', modelMeta[model].stateStore], storedData);
resolve(res.data);
}).catch(function(err) {
findQueries[queryId] = true;
debug.error('store:error', err);
reject(err);
});
});
findQueries[queryId] = p;
}
return findQueries[queryId];
}
/////////////////////
// GENERIC HELPERS //
/////////////////////
function setModel(model, data) {
// update the tree, setting models.[modelType].[modelId] to this model data
stateTree.set(['models', modelMeta[model].stateStore, data.id], new modelMeta[model].instance(data));
}
function get(url, query) {
var session = JSON.parse(localStorage.session);
var options = {
method: 'GET',
headers: {}
}
if (session && session.authToken) {
options.headers['Authorization'] = 'Bearer ' + session.authToken;
}
if (query) {
url += utils.queryToParams(query);
}
return fetch(__ENV__.API_HOST + url, options).then(function(res) {
return res.json();
});
}
/////////////
// EXPORTS //
/////////////
module.exports = {
models: models,
findOne: findOne,
find: find
}
'use strict';
// This mixin allows one to define a queries
// structure on components. The definition looks like this:
//
// mixins: [ stateMixins.query ],
// {
// queries: {
// post: {
// cursors: {
// posts: ['posts']
// },
// local: function(_this, cursors) {
// // _this is a reference to the component
// // cursors is an object of cursors defined above
// // so in this example you'd have access to cursors.posts
// // return the data or null
// },
// remote: function(_this, cursors) {
// // return a promise or null
// }
// }
// }
// }
var stateTree = require('./state'),
Combination = require('baobab/src/combination');
var QueryMixin = {
getInitialState() {
// Are there any queries to create?
if (!this.queries)
return {};
var initialState = {};
this.__bbQueries = {
listeners: [],
cursors: {}
};
// will be called when any of the
// queryCursors receives and update from the tree
this.queryUpdateHandler = (function(k) {
var d = {};
d[k] = this.queryfetchData(k);
// only update the state if the data has changed
if (this.state[k] !== d[k]) {
this.setState(d);
}
});
// used to populate the data for a particular query
this.queryfetchData = (function(queryIndex) {
var query = this.queries[queryIndex],
cursors = this.__bbQueries.cursors[queryIndex];
// Each query holds all it's meta data
// So if you have a query:
// queries: {
// post: {
// cursors: [],
// local: function(){}
// remote: function(){}
// }
// }
// this.state.post will equal the structure below
// so, for example, check this.state.post.loading to see if it's
// in the loading state, or this.state.post.result for the actual
// data returned from local/remote
var data = {
cursors: cursors,
loading: false,
error: null,
result: null
}
// Try the local function
data.result = query.local(this, cursors);
// No local data (either null or empty array)? Try remote
if ((!data.result || (data.result instanceof Array && data.result.length === 0)) && query.remote) {
var r = query.remote(this, cursors);
// query.remote returns a promise, or null
if (r && r instanceof Promise) {
data.loading = true;
var _this = this;
r.then(function() {
// no extra processing on success
// this is because an update to the tree will
// cause this function to be re-run anyways
}, function(err) {
// on error, set the error, because we don't
// update the tree in the error case
var d = _this.state[queryIndex];
d.loading = false;
d.error = err;
_this.setState(d);
});
} else {
// if query.remote returned null, we're not loading anything
data.loading = false;
}
}
return data;
}).bind(this);
// build the internal query structures for each defined query
for (var k in this.queries) {
var data = {},
query = this.queries[k];
// can't have a query without cursors
if (!query.cursors)
continue;
// Build the cursors and store them for this query
var queryCursors = {};
for (var j in query.cursors) {
queryCursors[j] = stateTree.select(query.cursors[j]);
}
this.__bbQueries.cursors[k] = queryCursors;
// Set the initial state for this query
// This sets this.state.{queryKey}
initialState[k] = this.queryfetchData(k);
}
return initialState;
},
componentDidMount() {
if (!this.queries) {
return;
}
// For each query, subscribe to the updates
// for the cursors involved in the query.
// When an update happens, call the updateHandler
// for this query.
for (var k in this.queries) {
var query = this.queries[k];
var combo = new Combination(
'or',
Object.keys(query.cursors).map(function(k) {
return stateTree.select(query.cursors[k]);
}, this)
);
combo.on('update', this.queryUpdateHandler.bind(this, k));
this.__bbQueries.listeners.push(combo);
}
},
componentWillUnmount() {
for (var k in this.__bbQueries.listeners) {
this.__bbQueries.listeners[k].release();
}
},
componentWillReceiveProps() {
// If the props have changed
// we might want to re-run the queries
// For example if using react-router and
// query.local or query.remote depends on
// a prop in the url
//
// we can probably make this smarter
for (var k in this.queries) {
this.queryUpdateHandler(k);
}
}
};
module.exports = {
query: QueryMixin
};
var Immutable = require('immutable');
class Post extends Immutable.Record({
id: null,
title: null,
body: null
}) {
// Methods go here
// foo() {
//
// }
};
module.exports = Post;
'use strict';
var React = require('react'),
{ State } = require('react-router'),
StateQuery = require('./query'),
StateActions = require('./actions'),
StateMixins = require('./mixins');
var Requests = React.createClass({
mixins: [ State, StateMixins.query ],
queries: {
post: {
cursors: {
posts: ['models', 'posts'],
},
local: function(_this, cursors) {
return StateQuery.findOne(cursors.posts.get(), {id: _this.getParams().postId});
},
remote: function(_this, cursors) {
return StateActions.findOne(StateActions.models.POST, {id: _this.getParams().postId}, false);
}
}
},
render() {
var post = this.state.post;
if (post.loading) {
return <div>Loading</div>;
} else if (post.error) {
return <div>Error: {post.error}</div>;
}
return <div>The post is {post.result.title}</div>;
},
});
module.exports = Post;
// Provides helper functions to filter
// data locally.
var StateActions = require('./actions'),
_find = require('lodash.find'),
_findWhere = require('lodash.findwhere'),
_sortByAll = require('lodash.sortbyall'),
_values = require('lodash.values');
function findOne(target, query) {
var found = false,
k;
var result = _find(target, function(i) {
found = true;
for (k in query) {
if (i.get(k) != query[k]) {
found = false;
}
}
return found;
});
return result ? result : null;
}
function find(target, query) {
// we want an array
if (target instanceof Object) {
target = _values(target);
}
var found = false,
result,
k;
if (query && Object.keys(query).length > 0) {
result = _findWhere(target, function(i) {
found = true;
for (k in query) {
if (i.get(k) != query[k]) {
found = false;
}
}
return found;
});
} else {
result = target;
}
return result && result instanceof Array ? result : [];
}
module.exports = {
findOne: findOne,
find: find
}
var Baobab = require('baobab');
var state = new Baobab({
models: {
posts: {}
}
});
// Easier debugging in the console
window.stateTree = state;
module.exports = state;
@jessep
Copy link

jessep commented Apr 6, 2015

Hi! This seems useful. Why a gist instead of a repo?

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