
redux-optimistic-ui
a reducer enhancer to enable type-agnostic optimistic updates
Installation
yarn add redux-optimistic-ui
A what-now?
A reducer enhance is a function you put around a reducer.
It can be your rootReducer (the output from a combineReducers
) or a nested one.
Optimistic-UI means you update what the client sees before the result comes back from the server.
This makes your app feel super fast, regardless of server location or internet connection speed.
How's it different from redux-optimist?
redux-optimistic-ui | redux-optimist |
---|
reducerEnhancer (wraps your state) | reducerExtender (adds an optimist to your state) |
can use immutable.js or anything else | must use plain JS objects for your state |
only uses 1 state copy | saves an extra copy of your state for every new optimistic action |
FSA compliant | not FSA compliant |
must wrap your state calls in ensureState | no change necessary to get your state |
Usage
Feed it your reducer
import {optimistic} from 'redux-optimistic-ui';
return optimistic(reducer);
This will transform your state so it looks like this:
state = {
history: [],
beforeState: <YOUR PREVIOUS STATE HERE>
current: <YOUR STATE HERE>
}
If the client is not waiting for a response from the server, the following are guaranteed to be true:
state.history.length === 0
state.beforeState === undefined
If you don't need to know if there is an outstanding fetch, you'll never need to use these.
Update your references to state
Since your state is now wrapped, you need state.current
.
But that sucks. What if you don't enhance the state until the user hits a certain route?
Lucky you! There's a function for that. ensureState
will give you your state whether it's enhanced or not.
Just wrap all your references to state
and getState
with it & you're all set!
getState().counter
import {ensureState} from 'redux-optimistic-ui'
ensureState(getState()).counter
Write some middleware
Now comes the fun! Not all of your actions should be optimistic.
Just the ones that fetch something from a server and have a high probability of success.
I like real-world examples, so this middleware is a little bit longer than the bare requirements:
import {BEGIN, COMMIT, REVERT} from 'redux-optimistic-ui';
const _SUCCESS = '_SUCCESS';
const _ERROR = '_ERROR';
let nextTransactionID = 0;
export default store => next => action => {
const {type, meta, payload} = action;
if (!meta || !meta.isOptimistic) return next(action);
let transactionID = nextTransactionID++;
next(Object.assign({}, action, {meta: {optimistic: {type: BEGIN, id: transactionID}}}));
socket.emit(type, payload, error => {
next({
type: type + (error ? _ERROR : _SUCCESS),
error,
payload,
meta: {
optimistic: error ? {type: REVERT, id: transactionID} : {type: COMMIT, id: transactionID}
}
});
})
};
Pro tips
Not using an optimistic-ui until a certain route? Using something like redux-undo
in other parts? Write a little something like this and call it on your asychronous route:
export default (newReducers, reducerEnhancers) => {
Object.assign(currentReducers, newReducers);
const reducer = combineReducers({...currentReducers})
if (reducerEnhancers){
return Array.isArray(reducerEnhancers) ? compose(...reducerEnhancers)(reducer) : reducerEnhancers(reducer);
}
return reducer;
}
Now you get an enhanced reducer only where you want it. Neat.
To see how it all comes together, check out https://github.com/mattkrick/meatier.