Skip to content

Instantly share code, notes, and snippets.

@mcrowe
Last active July 11, 2016 23:33
Show Gist options
  • Save mcrowe/f64f51da4e0a8acb1a9ae158dcde6f4d to your computer and use it in GitHub Desktop.
Save mcrowe/f64f51da4e0a8acb1a9ae158dcde6f4d to your computer and use it in GitHub Desktop.
Simple flux example
import React from 'react'
import { createConnector } from '../lib/connector'
import DB from './db'
import env from '../config/environment'
import Logger from './logger'
const DebugError = ({error, componentDisplayName}) =>
<div cl="connect-error-debug">
<div cl="connect-error-message">{error.message}</div>
{componentDisplayName &&
<div cl="connect-error-display-name">connecting props for {componentDisplayName}</div>
}
<div cl="connect-error-stack">{error.stack}</div>
</div>
const ProductionError = () =>
<div cl="connect-error-production">
<h2>Sorry, something went wrong.</h2>
<p><a href="/">Go back to safety</a></p>
</div>
const stores = [DB]
function renderError(error, componentDisplayName) {
if (env.debug) {
return <DebugError error={error} componentDisplayName={componentDisplayName} />
} else {
return <ProductionError />
}
}
function handleError(error, componentDisplayName) {
Logger.error(`Error connecting props for ${componentDisplayName}. ${error.message}`)
}
const connect = createConnector({stores, renderError, handleError})
export default connect
import React from 'react'
import { subscribeAll } from './store'
import assert from './assert'
function componentName(component) {
return component.displayName || component.name
}
/**
* @callback renderErrorCallback
* @param {Error} error - The error.
* @param {string} componentDisplayName - The display name of the component.
* @return A React component to render.
*/
/**
* @callback handleErrorCallback
* @param {Error} error - The error.
* @param {string} componentDisplayName - The display name of the component.
*/
/**
* Create a connector customized for your app's subscriptions.
* @param {Object[]} stores - An array of stores to subscribe to.
* The component will be re-rendered when any of these stores is updated.
* @param {renderErrorCallback} renderError - Render errors when resolving props.
* @param {handleErrorCallback=} handleError - Handle errors when resolving props.
*/
export const createConnector = ({stores, renderError, handleError}) => buildProps => component => {
assert(_.isArray(stores), "you must provide an array of 'stores' to subscribe to")
assert(_.isFunction(renderError), "you must provide a function 'renderError' to render errors when resolving props")
handleError = handleError || function() {}
return React.createClass({
getInitialState() {
return this.buildState()
},
componentWillReceiveProps(nextProps) {
this.setState(this.buildState())
},
componentWillMount() {
this.unsubscribe = subscribeAll(stores, () =>
this.setState(this.buildState())
)
},
componentWillUnmount() {
this.unsubscribe()
},
buildState() {
try {
return {
props: buildProps(this.props),
error: null
}
} catch(error) {
handleError(error, componentName(component))
return {
props: null,
error: error
}
}
},
render() {
if (this.state.error) {
const displayName = componentName(component)
return renderError(this.state.error, displayName)
} else {
return React.createElement(component, this.state.props, this.props.children)
}
}
})
}
// Stores are simple values that can be subscribed to, nothing
// more complicated than that.
// You can have multiple stores representing different objects.
const countStore = createStore(1)
countStore.subscribe(value => console.log('count updated with', value))
countStore.set(2)
// "connect" creates a higher-order component which subscribes to changes
// to all the stores you care about and re-renders the given component as needed.
// You could also subscribe manually to the stores in componentDidMount, but
// this avoids duplicating that logic.
const App = React.createClass({...})
connect(props => {
// Return the props to pass to the "App" component. This can access
// stores directly, or through convenience methods on services.
// It will get re-run when the stores change. This is wasteful in
// some cases, but that doesn't actually matter much in the real world.
return {
count: countStore.get()
}
})(App)
// Write your own service layer to make the store easier to work with.
// These are simple functions that set/get the value of stores directly.
// Actions and forced synchronous updates are beautiful theoretically,
// and can be really effective sometimes, but they definitely have a cost
// in Javascript. For many apps, this is much much simpler.
let Counter = {}
Counter.get = () => countStore.get()
Counter.set = (val) => countStore.set(val)
Counter.increment = () => Counter.set(Counter.get() + 1)
// ...
// Do you *really* need actions, action creators, reducers, files filled with constants, etc?
function createStore(initialValue) {
let subscriptions = []
let val = initialValue
function clone() {
return _.cloneDeep(val)
}
function broadcast() {
subscriptions.forEach(fn => fn(clone()))
}
return {
get() {
return clone()
},
set(newValue) {
val = _.cloneDeep(newValue)
broadcast()
},
update(fn) {
const newValue = fn(clone())
this.set(newValue)
},
subscribe(fn) {
subscriptions.push(fn)
// Call the subscription so that it gets executed with the
// initial value.
fn(clone(), INITIALIZE_LABEL)
// Return an unsubscribe function.
return function() {
const idx = subscriptions.indexOf(fn)
if (idx >= 0) {
subscriptions.splice(idx, 1)
}
}
}
}
}
export function subscribeAll(stores, cb) {
const unsubscribes = stores.map(store =>
store.subscribe(() =>
cb( stores.map(s => s.get()) )
)
)
// Return a function to unsubscribe to all stores.
return function() {
unsubscribes.forEach(fn => fn())
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment