Demonstration of the power and simplicity of React/FRP combo.
Flux does a nice job when abstracting views from the "business logic". However Flux application tend to be complex and have a slight enterprise smell with all those actions dispatchers, stores and listeners. This introduces a lot of boilerplate and unnecessary complexity to the codebase.
This TodoMVC project demonstrates how to bypass all those intermediate steps and keep your views clean with the power of FRP.
The most fundamental concept of Functional Reactive Programming (FRP) is the event stream. Streams are like arrays of events: they can be mapped, filtered, merged and combined.
The main idea is that every user action is just an event stream that is merged to the "application state" stream. Occurred events cause the application state to change. Finally the changed state object is then rendered at the top level of the application: React's virtual DOM does the rest!
This enables extremely simple React views. No callbacks and/or listener registrations are needed: the views only render what they are given and call synchronously the business logic interface on user actions. Business logic streams then propagate the state change back to views (which is again rendered stupidly). Nothing more is needed!
The essential component is the Dispatcher
which is basically just an object
of Bacon buses (Bacon.Bus
is a stream that can have data pushed into it. Equals Rx's Subject
).
const Bacon = require('baconjs')
module.exports = function() {
const busCache = {}
this.stream = function(name) {
return bus(name)
}
this.push = function(name, value) {
bus(name).push(value)
}
this.plug = function(name, value) {
bus(name).plug(value)
}
function bus(name) {
return busCache[name] = busCache[name] || new Bacon.Bus()
}
}
This dispatcher enables the easy emitting and listening of user actions:
const Bacon = require('baconjs'),
Dispatcher = require('./dispatcher')
const d = new Dispatcher()
module.exports = {
toItemsProperty: function(initialItems, filterS) {
const itemsS = Bacon.update(initialItems,
[d.stream('remove')], removeItem,
[d.stream('create')], createItem,
[d.stream('addState')], addItemState,
[d.stream('removeState')], removeItemState,
[d.stream('removeCompleted')], removeCompleteItems,
[d.stream('updateTitle')], updateItemTitle
)
return Bacon.combineAsArray([itemsS, filterS])
.map(withDisplayStatus)
function createItem(items, newItemTitle) {
return items.concat([{id: Date.now(), title: newItemTitle, states: []}])
}
function removeItem(items, itemIdToRemove) {
return R.reject(it => it.id === itemIdToRemove, items)
}
... rest of the business logic here ...
},
// "public" methods
createItem: function(title) {
d.push('create', title)
},
removeItem: function(itemId) {
d.push('remove', itemId)
},
... rest of the public interface here ...
}
Business logic can be added to state stream easily with Bacon.combineTemplate
:
const React = require('react'),
Bacon = require('baconjs'),
TodoApp = require('./todoApp'),
todos = require('./todos'),
filter = require('./filter')
const filterP = filter.toProperty(...intial filter state...),
itemsP = todos.toItemsProperty([], filterP)
const appState = Bacon.combineTemplate({
items: itemsP,
filter: filterP
})
appState.onValue((state) => {
React.render(<TodoApp {...state} />, document.getElementById('todoapp'))
})
After that, using your business logic is dead simple:
const todos = require('./todos')
...
<button onClick={() => todos.removeCompleted()}>Clear completed</button>
And note that view does not need to know if action is synchronous or asynchronous: it's up to business logic to decide that.
Feel free to clone the repository and start playing with the project:
git clone https://github.com/milankinen/react-bacon-todomvc.git
npm install
npm run watch
open "$(pwd)/index.html"
MIT