The power of
higher-order reducers
Daniel Bugl (omnidan)
whoami
- Daniel Bugl (omnidan)
- rackt member, redux maintainer
- creator of redux-undo, redux-ignore and redux-recycle higher-order reducers
- me on the internet
Re... ducks?
- reducer = (state, action) => state
- react-redux
- inject state and action creators as props
- when state changes, re-render
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Create example action for talk'
}
// a typical redux action
What is this talk about?
from redux-undo's README.md:
The problems
const initialState = {
past: [],
present: null, // (?) How do we initialize the present?
future: []
}
function undoable(state = initialState, action) {
const { past, present, future } = state
switch (action.type) {
case 'UNDO':
const previous = past[past.length - 1]
const newPast = past.slice(0, past.length - 1)
return {
past: newPast,
present: previous,
future: [ present, ...future ]
}
case 'REDO':
const next = future[0]
const newFuture = future.slice(1)
return {
past: [ ...past, present ],
present: next,
future: newFuture
}
default:
// (?) How do we handle other actions?
return state
}
}
The problems
- Problem 1: Where do we get the initial present state from? We don’t seem to know it beforehand.
- Problem 2: How do we actually delegate the control over the present state to a custom reducer?
- Problem 3: Where do we react to the external actions to save the present to the past?
The solution
- Reducer Enhancers / Higher-order Reducers
- are higher-order functions
functions that return functions (or take one or more functions as arguments)
// `createLogger`, a higher-order function (that returns another function)
function createLogger(name) {
return function logger(message) {
// this function can access `name` from the upper scope!
console.log(name + ': ' + message)
}
}
const log = createLogger('redux-undo')
log('undo action received') // prints out `redux-undo: undo action received`
The solution
- the same is possible with reducers
-
→ higher-order reducers
reducers that return reducers (and take a reducer function as argument)
// `doNothingWith`, a higher-order reducer (that does nothing at all!)
function doNothingWith(reducer) {
return function (state, action) {
// Just call the passed reducer
return reducer(state, action)
}
}
// `combineReducers`, a higher-order reducer (that combines other reducers!)
function combineReducers(reducers) {
return function (state = {}, action) {
return Object.keys(reducers).reduce((nextState, key) => {
// Call every reducer with the part of the state it manages
nextState[key] = reducers[key](state[key], action)
return nextState
}, {})
}
}
This is a redux utility function: http://rackt.org/redux/docs/api/combineReducers.html
The problems
- Problem 1: Where do we get the initial present state from? We don’t seem to know it beforehand.
- Problem 2: How do we actually delegate the control over the present state to a custom reducer?
- Problem 3: Where do we react to the external actions to save the present to the past?
The solution
function undoable(reducer) {
// Call the reducer with empty action to populate the initial state
const initialState = {
past: [],
present: reducer(undefined, {}), // Problem 1 solved!
future: []
}
// Return a reducer that handles undo and redo
return function (state = initialState, action) {
const { past, present, future } = state
switch (action.type) {
case 'UNDO':
// ...
case 'REDO':
// ...
default:
// Delegate handling the action to the passed reducer
const newPresent = reducer(present, action) // Problem 2 solved!
if (present === newPresent) {
return state
}
return {
past: [ ...past, present ], // Problem 3 solved!
present: newPresent,
future: []
}
}
}
}
Using undoable
// This is a reducer
function todos(state = [], action) {
/* ... */
}
// This is also a reducer!
const undoableTodos = undoable(todos)
import { createStore } from 'redux'
const store = createStore(undoableTodos)
store.dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
})
store.dispatch({
type: 'ADD_TODO',
text: 'Implement Undo'
})
store.dispatch({
type: 'UNDO'
})
- Note: Access state with state.present if it's been wrapped with undoable
redux-undo
- Installation:
npm install --save redux-undo
- New (1.0 beta; try it out, feedback's appreciated):
npm install --save redux-undo@beta
- Usage:
import { combineReducers } from 'redux' // Redux utility functions
import undoable from 'redux-undo' // redux-undo higher-order reducer
const reducers = combineReducers({
counter: undoable(counter, {
limit: 10 // set a limit for the history
})
})
redux-undo
- Note: Some higher-order reducers wrap your state!
{
todos: [...],
filter: 'active'
}
{
past: [...pastStatesHere...],
present: {
todos: [...],
filter: 'active'
},
future: [...futureStatesHere...]
}
From:
reducer
To:
undoable(reducer)
redux-undo
- Dispatching undo/redo actions:
import { ActionCreators } from 'redux-undo'
store.dispatch(ActionCreators.undo()) // undo the last action
store.dispatch(ActionCreators.redo()) // redo the last action
// jump to requested index in the past[] array
store.dispatch(ActionCreators.jumpToPast(index))
// jump to requested index in the future[] array
store.dispatch(ActionCreators.jumpToFuture(index))
redux-undo
- Filtering actions:
undoable(reducer, {
filter: function filterActions(action, currentState, previousState) {
// only add to history if action is SOME_ACTION
return action.type === SOME_ACTION
}
})
// or you could do...
undoable(reducer, {
filter: function filterState(action, currentState, previousState) {
// only add to history if state changed
return currentState !== previousState
}
})
// or with helpers...
import undoable, { distinctState, includeAction, excludeAction } from 'redux-undo'
undoable(reducer, { filter: includeAction(SOME_ACTION) })
undoable(reducer, { filter: excludeAction(SOME_ACTION) })
undoable(reducer, { filter: includeAction([SOME_ACTION, SOME_OTHER_ACTION]) })
undoable(reducer, { filter: excludeAction([SOME_ACTION, SOME_OTHER_ACTION]) })
undoable(reducer, { filter: distinctState() })
Live demo!
Making an existing app (todos) undoable (todos-with-undo)
with redux-undo
Check out the result: rackt/redux > examples/todos-with-undo
Enjoy the superpowers!
-
Now that you know this:
- Use higher-order functions more (really useful in some cases)
- Try out redux-undo in your own project
- Create your own higher-order reducer
- My other projects:
- If you ever want to ignore some actions for some reducers (e.g. for performance reasons): redux-ignore
- Or if you want to reset the state on some actions: redux-recycle
- node-emoji, coffea (messaging library)
- Feel free to ask me about any of my projects!
Where to find me
- me in real life
- Daniel Bugl (omnidan)
- Vienna, Austria
- at local meetups (e.g. Vienna ReactJS)
- me on the internet
- omnidan.net
- github.com/omnidan
- twitter.com/DanielBugl
- me@omnidan.net
-
bit.ly/react-vienna (come chat with us!)
- don't forget the link: slides.com/omnidan/hor
I hope you learned something useful today!
Useful links
- redux-undo: https://github.com/omnidan/redux-undo
- redux-undo-boilerplate: https://github.com/omnidan/redux-undo-boilerplate
- redux-ignore: https://github.com/omnidan/redux-ignore
- redux-recycle: https://github.com/omnidan/redux-recycle
- elm: http://elm-lang.org/
- elm architecture: https://github.com/evancz/elm-architecture-tutorial
- elm-undo-redo: https://github.com/TheSeamau5/elm-undo-redo
- Redux docs (very good resource for learning redux): http://rackt.org/redux/
- Undo History recipe (explains how redux-undo and higher-order reducers work): http://rackt.org/redux/docs/recipes/ImplementingUndoHistory.html
The power of higher-order reducers
By omnidan
The power of higher-order reducers
Enhance the functionality of your existing reducers by wrapping them with a higher-order reducer, like redux-undo's undoable, which adds undo/redo functionality.
- 32,981