Skip to content

Instantly share code, notes, and snippets.

@SimonDegraeve
Last active August 29, 2015 14:15
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 SimonDegraeve/fab1e3bb45a10f81a324 to your computer and use it in GitHub Desktop.
Save SimonDegraeve/fab1e3bb45a10f81a324 to your computer and use it in GitHub Desktop.
ConnectStores method for Flummox (like Reflux connect mixin)
/*
* Import libraries
*/
import React from 'react';
/*
* Define helpers
*/
function capitalize(str) {
return (str.charAt(0).toUpperCase() + str.slice(1)).replace(/\s/g,'');
}
/*
* Define and export App component
*
* This class get a Flux instance as 'flux' prop
* and expose it as 'flux' context to his children
*
* We start with a prop to avoid React warning about
* 'owner-based and parent-based contexts differ'
* See https://gist.github.com/jsfb/0eb6e61f300a8c1b2ce7
*/
export default class App extends React.Component {
// Flux/context specific
static get propTypes() {
return {
flux: React.PropTypes.object.isRequired
}
}
static get childContextTypes() {
return {
flux: React.PropTypes.object.isRequired
}
}
getChildContext() {
return { flux: this.props.flux };
}
// Component specific
constructor(props, context) {
super(props, context);
// Bind instance methods
this.connectStores = this.connectStores.bind(this);
// Connect store state with local state
//
// - If connectStores() get store instance:
// default listener is `onMyStoreChange` (construct from `on` + store displayName + `Change`)
// if `onMyStoreChange` does not exist then default is `function() { this.setState(myStore.state); };`
// - Else connectStores() expect spec object:
// ex: { store: myStore, key: 'myCustomKey', callback: this.myCustomCallback }
// NB: if callback not provided then default to `on` + key + `Change` (ex: `OnMyCustomKeyChange`)
// if key not provided then default to store displayName
//
// State is automatically merged from store state
// Stores can be accessed in this.stores.myStore (default) or this.stores.myCustomKey if using object spec
var myStore = props.flux.getStore('myStore');
this.connectStores([
myStore
// some other stores
// ...
]);
// Or
// this.connectStores([
// { store: myStore, key: 'myStore', callback: this.onMyStoreChange }
// // some other stores
// // ...
// ]);
}
connectStores(storesSpec = []) {
// Check if method already called
if (this._hasConnectedStores) {
throw Error('`connectStores()` must be called only once per component.')
}
// Set boolean to track method call
this._hasConnectedStores = true;
// Set easy access to stores in component instance
this.stores = {};
// Normalize stores specs
this._storesSpec = storesSpec.map(storeSpec => {
// Normalize spec if store instance provided
if (typeof storeSpec._token !== 'undefined') {
storeSpec = { store: storeSpec };
}
// Set default key if not provided
if (typeof storeSpec.key === 'undefined') {
storeSpec.key = storeSpec.store.constructor.displayName;
}
// Set default callback if not provided
if (typeof storeSpec.callback === 'undefined') {
var callbackName = 'on' + capitalize(storeSpec.key) + 'Change';
if (typeof this[callbackName] === 'undefined') {
this[callbackName] = function() {
this.setState(storeSpec.store.state);
}.bind(this);
}
storeSpec.callback = this[callbackName].bind(this);
}
// Add store to the instance list of stores
this.stores[storeSpec.key] = storeSpec.store;
// Merge store state with instance state
this.state = Object.assign({}, this.state, storeSpec.store.state);
// Return normalize spec
return storeSpec;
});
// Add/remove listeners in component lifecycle methods
var _componentDidMount = this.componentDidMount || function() {};
this.componentDidMount = function() {
this._storesSpec.forEach(storeSpec => {
storeSpec.store.addListener('change', storeSpec.callback);
});
_componentDidMount.call(this);
};
var _componentWillUnmount = this.componentWillUnmount || function() {};
this.componentWillUnmount = function() {
this._storesSpec.forEach(storeSpec => {
storeSpec.store.removeListener('change', storeSpec.callback);
});
_componentWillUnmount.call(this);
};
}
render() {
return null;
}
}
/*
* Import libraries
*/
import { Flux } from 'flummox';
/*
* Import stores/actions
*/
import MyStore from './MyStore';
/*
* Define and export AppFlux flux
*/
export default class AppFlux extends Flux {
constructor() {
super();
// Create some actions
// ...
// Create some stores
// NB: Would be nice to have shortcut if first argument is not a string then
// use the `displayName` static prop
// ex:
// this.createStore(ContentStore, this);
this.createStore(ContentStore.displayName, ContentStore, this);
}
}
/*
* Import libraries
*/
import '6to5/register';
import Flux from './AppFlux';
import React from 'react';
/*
* Import components
*/
import App from './AppComponent';
/*
* Render application
*/
var flux = new Flux();
React.render(<App flux={flux}/>, document.body)
/*
* Import libraries
*/
import { Store } from 'flummox';
/*
* Define and export MyStore store
*/
export default class MyStore extends Store {
// NB: Would be nice to have this native
//
// Rely on this static prop because cannot trust
// constructor name if script mangled (ex: uglifyjs)
//
// Or
// MyStore.displayName = 'myStore';
static get displayName() {
return 'myStore';
}
constructor(flux) {
super();
// Register some actions
// ...
// Set some state
// ...
}
}
@acdlite
Copy link

acdlite commented Feb 11, 2015

What's the advantage of the static displayName property? Instead of defining the name in createStore, you're defining it some place else... Is there really any benefit?

The connectStores implementation is interesting. Kinda similar to what I'm working on. I don't know how I feel about the default listener behavior, though. What's an example use case for listener callbacks if state is automatically being merged, anyway?

@acdlite
Copy link

acdlite commented Feb 11, 2015

Oh I see, it's not doing what I thought it was on first glance. Hmm.

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