redux-optimist
Optimistically apply actions that can be later commited or reverted.
Installation
npm install redux-optimist
Usage
Step 1: Wrap your top level reducer in redux-optimist
reducers/todos.js
export default function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.text]);
default:
return state;
}
}
reducers/status.js
export default function status(state = {writing: false, error: null}, action) {
switch (action.type) {
case 'ADD_TODO':
return {writing: true, error: null};
case 'ADD_TODO_COMPLETE':
return {writing: false, error: null};
case 'ADD_TODO_FAILED':
return {writing: false, error: action.error};
default:
return state;
}
}
reducers/index.js
import optimist from 'redux-optimist';
import { combineReducers } from 'redux';
import todos from './todos';
import status from './status';
export default optimist(combineReducers({
todos,
status
}));
As long as your top-level reducer returns a plain object, you can use optimist. You don't
have to use Redux.combineReducers
.
Step 2: Mark your optimistic actions with the optimist
key
middleware/api.js
import {BEGIN, COMMIT, REVERT} from 'redux-optimist';
import request from 'then-request';
let nextTransactionID = 0;
export default function (store) {
return next => action => {
if (action.type !== 'ADD_TODO') {
return next(action);
}
let transactionID = nextTransactionID++;
next({
type: 'ADD_TODO',
text: action.text,
optimist: {type: BEGIN, id: transactionID}
});
request('POST', '/add_todo', {text: action.text}).getBody().done(
res => next({
type: 'ADD_TODO_COMPLETE',
text: action.text,
response: res,
optimist: {type: COMMIT, id: transactionID}
}),
err => next({
type: 'ADD_TODO_FAILED',
text: action.text,
error: err,
optimist: {type: REVERT, id: transactionID}
})
);
}
};
Note how we always follow up by either COMMITing the transaction or REVERTing it. If you do neither, you will get a memory leak. Also note that we use a serialisable transactionID such as a number. These should always be unique accross the entire system.
Step 3:
Using this, we can safely fire off ADD_TODO
actions in the knowledge that the UI will update optimisticly, but will revert if the write to the server fails.
App.js
import { createStore, applyMiddleware } from 'redux';
import api from './middleware/api';
import reducer from './reducers';
// Note: passing middleware as the last argument to createStore requires redux@>=3.1.0
let store = createStore(reducer, applyMiddleware(api));
console.log(store.getState());
// {
// optimist: {...},
// todos: [],
// status: {writing: false, error: null}
// }
store.dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
});
console.log(store.getState());
// {
// optimist: {...},
// todos: ['Use Redux'],
// status: {writing: true, error: null}
// }
// You can apply other actions here and their updates won't get lost
// even if the original ADD_TODO action gets reverted.
// Some time later...
console.log(store.getState());
// either
// {
// optimist: {...},
// todos: ['Use Redux'],
// status: {writing: false, error: null}
// }
// or
// {
// optimist: {...},
// todos: [],
// status: {writing: false, error: Error}
// }
License
MIT