How Do You Todo? A Microcosm / Redux Comparison
Eli Fatsi, Former Development Director
Article Categories:
Posted on
Comparing Microcosm and Redux, two powerful libraries for managing application state
For those who don't know, we've been working on our own React framework here at Viget called Microcosm. Development on Microcosm started before Redux had hit the scene and while the two share a number of similarities, there are a few key differences we'll be highlighting in this post.
I've taken the Todo app example from Redux's docs (complete app forked here), and implemented my own Todo app in Microcosm. We'll run through these codebases side by side comparing how the two frameworks help you with different developer tasks. Enough chatter, let's get to it!
Entry point #
So you've yarnpm installed the dependency, now what?
// Redux
// index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers/index'
import App from './components/App'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
// Microcosm
// repo.js
import Microcosm from 'microcosm'
import Todos from './domains/todos'
import Filter from './domains/filter'
export default class Repo extends Microcosm {
setup () {
this.addDomain('todos', Todos)
this.addDomain('currentFilter', Filter)
}
}
// index.js
import { render } from 'react-dom'
import React from 'react'
import Repo from './repo'
import App from './presenters/app'
const repo = new Repo()
render(
<App repo={repo} />,
document.getElementById('root')
)
Pretty similar looking code here. In both cases, we're mounting our App
component to the root element and setting up our state management piece. Redux has you creating a Store, and passing that into a wrapping Provider component. With Microcosm you instantiate a Repo instance and set up the necessary Domains. Since Microcosm Presenters (from which App
extends) take care of the same underlying "magic" access to the store/repo, there's no need for a higher-order component.
State Management #
This is where things start to diverge. Where Redux has a concept of Reducers, Microcosm has Domains (and Effects, but we won't go into those here). Here's some code:
// Redux
// reducers/index.js
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
const todoApp = combineReducers({
todos,
visibilityFilter
})
export default todoApp
// reducers/todos.js
const todo = (state = {}, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
}
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state
}
return Object.assign({}, state, {
completed: !state.completed
})
default:
return state
}
}
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
]
case 'TOGGLE_TODO':
return state.map(t =>
todo(t, action)
)
default:
return state
}
}
export default todos
// reducers/visibilityFilter.js
const visibilityFilter = (state = 'SHOW_ALL', action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
export default visibilityFilter
// Microcosm
// domains/todos.js
import { addTodo, toggleTodo } from '../actions'
class Todos {
getInitialState () {
return []
}
addTodo (state, todo) {
return state.concat(todo)
}
toggleTodo (state, id) {
return state.map(todo => {
if (todo.id === id) {
return {...todo, completed: !todo.completed}
} else {
return todo
}
})
}
register () {
return {
[addTodo] : this.addTodo,
[toggleTodo] : this.toggleTodo
}
}
}
export default Todos
// domains/filter.js
import { setFilter } from '../actions'
class Filter {
getInitialState () {
return "All"
}
setFilter (_state, newFilter) {
return newFilter
}
register () {
return {
[setFilter] : this.setFilter
}
}
}
export default Filter
There are some high level similarities here: we're setting up handlers to deal with the result of actions and updating the application state accordingly. But the implementation differs significantly.
In Redux, a Reducer is a function which takes in the current state and an action, and returns the new state. We're keeping track of a list of todos
and the visibilityFilter
here, so we use Redux's combineReducers
to keep track of both.
In Microcosm, a Domain is a class built to manage a section of state, and handle actions individually. For each action, you specify a handler function which takes in the previous state, as well as the returned value of the action, and returns the new state.
In our Microcosm setup, we called addDomain('todos', Todos)
and addDomain('currentFilter', Filter)
. This hooks up our two domains to the todos
and currentFilter
keys of our application's state object, and each domain becomes responsible for managing their own isolated section of state.
A major difference here is the way that actions are handled on a lower level, and that's because actions themselves are fundamentally different in the two frameworks (more on that later).
Todo List #
Enough with the behind-the-scenes stuff though, let's take a look at how the two frameworks enable you to pull data out of state, display it, and trigger actions. You know - the things you need to do on every React app ever.
// Redux
// containers/VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
default:
return todos
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
// components/TodoList.js
import React from 'react'
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map(todo =>
<li
key = {todo.id}
onClick = {() => onTodoClick(todo.id)}
style = {{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
>
{todo.text}
</li>
)}
</ul>
)
export default TodoList
// Microcosm
// presenters/todoList.js
import React from 'react'
import Presenter from 'microcosm/addons/presenter'
import { toggleTodo } from '../actions'
class VisibleTodoList extends Presenter {
getModel () {
return {
todos: (state) => {
switch (state.currentFilter) {
case 'All':
return state.todos
case 'Active':
return state.todos.filter(t => !t.completed)
case 'Completed':
return state.todos.filter(t => t.completed)
default:
return state.todos
}
}
}
}
handleToggle (id) {
this.repo.push(toggleTodo, id)
}
render () {
let { todos } = this.model
return (
<ul>
{todos.map(todo =>
<li
key={todo.id}
onClick={() => this.handleToggle(todo.id)}
style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
>
{todo.text}
</li>
)}
</ul>
)
}
}
export default VisibleTodoList
So with Redux the setup detailed here is, shall we say ... mysterious? Define yourself some mapStateToProps
and mapDispatchToProps
functions, pass those into connect
, which gives you a function, which you finally pass your view component to. Slightly confusing at first glance and strange that your props become a melting pot of state and actions. But, once you become familiar with this it's not a big deal - set up the boiler plate code once, and then add the meat of your application in between the lines.
Looking at Microcosm however, we see the power of a Microcosm Presenter. A Presenter lets you grab what you need out of state when you define getModel
, and also maintains a reference to the parent Repo so you can dispatch actions in a more readable fashion. Presenters can be used to help with simple scenarios like we see here, or you can make use of their powerful forking functionality to build an "app within an app" (David Eisinger wrote a fantastic post on that), but that's not what we're here to discuss, so let's move on!
Add Todo #
Let's look at what handling form input looks like in the two frameworks.
// Redux
// containers/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
let AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form
onSubmit={e => {
dispatch(addTodo(input.value))
}}
>
<input ref={node => {input = node}} />
<button type="submit">Add Todo</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
// Microcosm
// views/addTodo.js
import React from 'react'
import ActionForm from 'microcosm/addons/action-form'
import { addTodo } from '../actions'
let AddTodo = () => {
return (
<div>
<ActionForm action={addTodo}>
<input name="text" />
<button>Add Todo</button>
</ActionForm>
</div>
)
}
export default AddTodo
With Redux, we again make use of connect
, but this time without any of the dispatch/state/prop mapping (just when you thought you understood how connect
worked). That passes in dispatch
as an available prop to our functional component which we can then use to send actions out.
Microcosm has a bit a syntactic sugar for us here with the ActionForm
addon. ActionForm will serialize the form data and pass it along to the action you specify (addTodo
in this instance). Along these lines, Microcosm provides an ActionButton
addon for easy button-to-action functionality, as well as withSend
which operates similarly to Redux's connect
/dispatch
combination if you like to keep things more low-level.
In the interest of time, I'm going to skip over the Filter Link implementations, the comparison is similar to what we've already covered.
Actions #
The way that Microcosm handles Actions is a major reason that it stands out in the pool of state management frameworks. Let's look at some code, and then I'll touch on some high level points.
// Redux
// actions/index.js
let nextTodoId = 0
export const addTodo = text => {
return {
type: 'ADD_TODO',
id: nextTodoId++,
text
}
}
export const setVisibilityFilter = filter => {
return {
type: 'SET_VISIBILITY_FILTER',
filter
}
}
export const toggleTodo = id => {
return {
type: 'TOGGLE_TODO',
id
}
}
// Microcosm
// actions/index.js
let nextTodoId = 0
export function addTodo(data) {
return {
id: nextTodoId++,
completed: false,
text: data.text
}
}
export function setFilter(newFilter) {
return newFilter
}
export function toggleTodo(id) {
return id
}
At first glance, things look pretty similar here. In fact, the only major difference in defining actions here is the use of action types in Redux. In Microcosm, domains register to the actions themselves instead of a type constant, removing the need for that set of boilerplate code.
The important thing to know about Microcosm actions however is how powerful they are. In a nutshell, actions are first-class citizens that get things done, and have a predictable lifecycle that you can make use of. The simple actions here return JS primitives (similar to our Redux implementation), but you can write these action creators to return functions, promises, or generators (observables supported in the next release).
Let's say you return a promise that makes an API request. Microcosm will instantiate the action with an open
status, and when the promise comes back, the action's status will update automatically to represent the new situation (either update
, done
, or error
). Any Domains (guardians of the state) that care about that action can react to the individual lifecycle steps, and easily update the state depending on the current action status.
Action History #
The last thing I'll quickly cover is a feature that is unique to Microcosm. All Microcosm apps have a History, which maintains a chronological list of dispatched actions, and knows how to reconcile action updates in the order that they were pushed. So if a handful of actions are pushed, it doesn't matter in what order they succeed (or error out). Whenever an Action changes its status, History alerts the Domains about the Action, and then moves down the chronological line alerting the Domains of any subsequent Actions as well. The result is that your application state will always be accurate based on the order in which actions were dispatched.
This topic deserves its own blog post to be honest, it's such a powerful feature that takes care of so many problems for you, but is a bit tough to cram into one paragraph. If you'd like to learn more, or are confused by my veritably confusing description, check out the History Reconciling docs.
Closing Thoughts #
Redux is a phenomenal library, and the immense community that's grown with it over the last few years has brought forth every middleware you can think of in order to get the job done. And while that community has grown, we've been plugging away on Microcosm internally, morphing it to suit our ever growing needs, making it as performant and easy to use as possible because it makes our jobs easier. We love working with it, and we'd love to share the ride with anyone who's curious.
Should you be compelled to give Microcosm a go, here are a few resources to get you running: