Last active
March 30, 2018 06:29
-
-
Save TheDahv/bbb4ee9493db06222aa3b858ed8dfc78 to your computer and use it in GitHub Desktop.
State flow control (Redux, Flux, etc.) in 100 lines or less
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This example tries to show how a one-way state flow control library--similar | |
* to libraries like Redux, Flow, Elm, etc.--could work. For a quick refresher | |
* on the problem we're solving, we want a way to: | |
* | |
* - make the state of our app easier to find and express | |
* - confine changes to our state to clear, simple, pure functions | |
* - update the app in response to events--either from the user or from the | |
* browser | |
* | |
* To borrow from Elm language, we want to establish a "model -> update -> view" | |
* flow. | |
* | |
* I started thinking about web applications as a stream of events describing | |
* user behavior over time. Then I thought about the UI of the application being | |
* the result of events and application state applied to update functions. | |
* | |
* When I work on streams problems, I always turn to Highland.js [0]. Highland | |
* gives me an elegant, high-level API to work with a stream of events and | |
* produce a new version of the UI in response to each event. In effect, the | |
* application is a reduction of the events and the application state over time. | |
* | |
* This isn't an example of something that should be put in production. I can't | |
* even recommend it as a new way to solve your web application management | |
* problems. But I do want to demystify some of the magic happening in your web | |
* frameworks and show how thinking of old problems in new ways can be really | |
* fun. The uncommented, ugly-code version of this fits in at less than 100 | |
* lines, so I hope you're able to get something from this as I attempt to make | |
* it simple enough to comprehend. | |
* | |
* Read on to get a sense for what's going on and hopefully you have as much fun | |
* as I did! | |
* | |
* -- David | |
* | |
* [0] - https://highlandjs.org/ | |
*/ | |
/** | |
* First we pull in preact, which is a lightweight React-compatible library. | |
* There's no specific reason for me to use it over React for this problem other | |
* than that it's very lightweight | |
*/ | |
const { h, render } = preact; | |
/** | |
* This list of actions describe the kind of events that can happen in our | |
* application. The only reason for this fancy-pants object building is just to | |
* give us a way to enumerate our actions and make it harder to make typos | |
* later. | |
*/ | |
const actions = [ | |
'CLOCK_UPDATE', 'COUNTER_ADD', 'COUNTER_SUBTRACT', | |
'FUTURE_MESSAGE', 'NAME_INPUT' | |
].reduce((memo, action) => Object.assign(memo, { [action]: action }), {}); | |
/** | |
* This is where we start with Highland. Highland can wrap other stream-like | |
* objects (callbacks, events, generators, promises, arrays, etc.), but an empty | |
* Highland object can serve as both a readable and writable stream. This will | |
* be our global event bus. | |
*/ | |
const bus = highland(); | |
/** | |
* For sake of familiarity with an API you may already be familiar with, we set | |
* up a dispatch helper that we can pass around to components to send events | |
* into our bus. An event is just data: the kind of event, and any relevant metadata. | |
*/ | |
const dispatch = (event = {}) => bus.write(event); | |
// run takes a DOM node, sets up the event stream processor, and kicks off the | |
// first render with the application's "empty" state. | |
function run(container) { | |
// Here we set up the beginning default empty state of the app | |
let initial = { count: 0, currentTime: new Date, futureMessage: '', name: '' }; | |
/** | |
* After we run the initial render, we save a reference to Preact's render | |
* This lets Preact know to use its DOM diffing algorithm to replace the | |
* contents of the container rather than appending each time: | |
* https://preactjs.com/guide/api-reference | |
*/ | |
let replaceNode = render(h(App, { ...initial }), container); | |
/** | |
* Here we have some helper functions to deal with events in the stream. We | |
* want to be able to both handle events that describe themselves | |
* synchronously, or events that trigger at some future point in time. For | |
* that, we use promises, so we need to split the event stream to handle them | |
* differently. | |
*/ | |
const promise = event => typeof event.then === 'function'; | |
// syncEvents are data that we can process right away. | |
const syncEvents = bus.fork().reject(promise).tap(console.log); | |
/** | |
* We set up a stream of asynchronous events so we can listen for them to | |
* resolve and then return them to the dispatch stream. This is important | |
* because we don't want to block the stream of synchronous events that we can | |
* process right now. | |
*/ | |
const asyncEvents = bus.fork().filter(promise).each((promise) => promise.then(dispatch)); | |
// We process the two normalized stream events together as they come in | |
highland.merge([ syncEvents, asyncEvents ]). | |
/** | |
* At this point, we want to take an event and the state of the application, | |
* and produce a new version of the state based on the changes indicated by | |
* the event. | |
* | |
* We could think of the event processing like a list reduction: given an | |
* initial value, a list of items, and a function combine the state and item | |
* to produce new values through the list, we can arrive at a final version | |
* of that value. | |
* | |
* For our app, the state is the initial value, and we reduce to the final | |
* state of the UI after applying all the changes. | |
* | |
* However, this is an endless stream of application events and we want the | |
* UI to update the whole time, not just at the end. | |
* | |
* For that we turn to scan [0]. It is just like reduce, but it also emits the | |
* each value as it is reducing over the event stream. That's great because | |
* we can then produce new HTML for each version of the app state. | |
* | |
* [0] http://highlandjs.org/#scan | |
*/ | |
scan(initial, reduce). | |
/** | |
* Here we take each version of the app state and render it. We *could* work | |
* it in to the stream values, but HTML updates are a side-effect and all | |
* the code up to this point has been pure. We use Preact to render the | |
* top-level App component at each state update, using the 'replaceNode' we | |
* saved earlier so Preact knows to perform an update and not an append. | |
*/ | |
each(state => render(h(App, { ...state }), container, replaceNode)). | |
// We want to 'consume' our Stream. Otherwise we just set up a large | |
// description of what *will* happen without ever actually kicking off the | |
// stream flow. | |
done(() => console.log('Ran out of events!')); | |
// By now, we have a way to pass state to our View code, and we can respond to | |
// changes. Let's show some of the asynchronous ways to trigger changes first. | |
// Here we just loop on every second and dispatch the current time. Since the | |
// 'dispatch' function is in scope here, we can just call it. | |
setInterval(() => dispatch({ type: 'CLOCK_UPDATE', time: new Date }), 1000); | |
// Here we show an example of dispatching an event whose data we don't know | |
// right at the moment. This could be something like waiting for a server to | |
// respond to an API call. Because our event stream knows how to handle | |
// Promises/Futures, this lets us handle async problems with an API pretty | |
// familiar to JS programmers. | |
dispatch(new Promise(resolve => { | |
const msg = {type: actions.FUTURE_MESSAGE, message: 'hi from the future'}; | |
setTimeout(resolve.bind(null, msg), 2000); | |
})); | |
} | |
/** | |
* The reduce function is responsible for producing a new version of the | |
* application state in response to an event/action. It's actually kind of | |
* boring! That's good though since state management is usually where bugs come | |
* from so we want this to be simple. | |
* | |
* If this appplication were bigger, we would be either composing other | |
* collections of data-producing functions that deal with subsets of the | |
* application domain, or forwarding events on to sub-reducers to organize our | |
* code better. | |
*/ | |
function reduce(state, event) { | |
let updated = Object.assign({}, state, (() => { | |
switch (event.type) { | |
default: return updated; | |
case actions.NAME_INPUT: return { name: event.name }; | |
case actions.COUNTER_ADD: return { count: state.count + 1 }; | |
case actions.COUNTER_SUBTRACT: return { count: state.count - 1 }; | |
case actions.CLOCK_UPDATE: return { currentTime: event.time }; | |
case actions.FUTURE_MESSAGE: return { futureMessage: event.message }; | |
} | |
})()); | |
// Quick and dirty logging like https://www.npmjs.com/package/redux-logger | |
console.log({ event, state, updated }); | |
return updated; | |
}; | |
/** | |
* App is the top-level component responsible for passing the pieces of state | |
* that are relevant to the smaller, more focused components in the app. This is | |
* similar to Redux' notion of a 'container'. It mostly exists as the bridge | |
* between the state management part of the framework and the pure UI rendering | |
* functions. | |
* | |
* We're not using JSX in any of this code, but the 'h' function is pretty | |
* simple to follow and I've nested the components to mimic the hierarchy | |
* you're used to seeing in HTML/JSX. | |
*/ | |
function App(state) { | |
const { count, currentTime, futureMessage, name } = state; | |
return h('div', null, | |
// This example shows simple binding of state to input controls. Our HTML | |
// bits are scattered around, but it's easy to see how the input and output | |
// work together. | |
h('div', { className: 'form' }, | |
// We hid some of the update behavior inside of NameForm component, so go | |
// look at that! | |
h(NameForm, { name })), | |
h('div', null, | |
h('p', null, `Hello ${name}`)), | |
// Here we wrap even more functionality into a higher-level component that | |
// will compose more components. This implements the usual demo this | |
// family of frameworks employs: http://elm-lang.org/examples/buttons | |
h(Counter, { count, className: 'counter' }), | |
// Clock is where we represent the setInterval clock tick we set up earlier | |
h(Clock, { time: currentTime }), | |
// And here we have a place to show the results of our Promise stream | |
// processing | |
h('div', null, | |
h('p', null, | |
h('strong', null, futureMessage))) | |
); | |
} | |
/** | |
* NameForm is our first example of hiding component details inside a component | |
* function. Notice how we define what fields we want from the state and then | |
* arrange our first event dispatching. | |
*/ | |
function NameForm({ name } = {}) { | |
return h('input', { | |
type: 'text', placeholder: 'Name', value: name, | |
// Here we hook into Preact's component events to dispatch to the event | |
// stream | |
onInput: (evt) => { | |
dispatch({ type: 'NAME_INPUT', name: evt.target.value }); | |
}}); | |
} | |
/** | |
* Counter demonstrates composing smaller, simple custom components to wire up | |
* related behavior to our event dispatcher | |
*/ | |
function Counter({ count, className } = {}) { | |
return h('div', { className }, | |
h(CounterButton, { action: 'COUNTER_ADD', text: '+' }), | |
h('span', null, count), | |
h(CounterButton, { action: 'COUNTER_SUBTRACT', text: '-' })); | |
} | |
function CounterButton({ action, text } = {}) { | |
return h('button', { onClick: dispatch.bind(null, { type: action }), }, text); | |
} | |
function Clock({ time } = {}) { | |
return h('div', { className: 'clock' }, | |
h('span', null, 'The time is: '), | |
h('strong', null, time.toJSON())); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>State flow control (Redux, Flux, etc.) in 100 lines or less</title> | |
</head> | |
<body> | |
<div id="app"></div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/highland/2.13.0/highland.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/preact/dist/preact.min.js"></script> | |
<script src="./annotated-app.js"></script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment