// 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')
)
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.// 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
todos
and the visibilityFilter
here, so we use Redux's combineReducers
to keep track of both.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.// 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
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.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!// 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
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.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.// 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
}
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.