redux-optimism

Enhances your redux store with optimism.


Keywords
react, redux, state, optimistic
License
MIT
Install
npm install redux-optimism@0.0.1-alpha.5

Documentation

Build Status Coverage Status

redux-optimism

Enhances your redux store with optimism.

Take care, this package is in alpha stage!

Installation

Just

npm install redux-optimism

// or
yarn add redux-optimism 

Usage

The module was designed with redux-thunk in mind (like in the example). It'll probably work well with other async patterns though.

1. Enhance your reducer

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import {makeOptimistic} from 'redux-optimism'

import rootReducer from './reducers/index';

const store = createStore(
    makeOptimistic(rootReducer),
    applyMiddleware(thunk)
);

2. Dispatch with optimism

import {getOptimism} from 'redux-optimism'

// Our thunk
function thunkAction() {
    return async dispatch => {
        let {optimistic, commit, rollback} = getOptimism(dispatch)
        optimistic({type: 'UPDATE_DATA'})
        optimistic({type: 'UPDATE_OTHER_DATA'})
        try {
            await apiCall()
            commit()    
            dispatch({type: 'OTHER_ACTION'})
        } catch (e) {
            rollback()   
        }
    }
}

Remarks

Benefits

  • You can dispatch as many optimistic actions as you like.
  • Actions after an optimistic action won't get lost.
  • The state is also calculated correctly when you have different optimistic sessions ("optimisms"), that get rolled back or committed in random order. Just keep in mind, that no non-optimistic action may happen that depends on an optimistic action. For example there mustn't be a delete key for an optimistically created item.
  • Fully compatible to Flux Standard Actions.
  • Thoroughly typed — get the full power of your IDE.
  • Zero dependencies.

Pitfalls

  • This package is alpha! It's functionality is well tested, but I don't know, how well or badly it integrates with other redux stuff. Please open an issue if you find a failing example!
  • Optimistic updates reduce the amount of comprehensibility in your store.
  • redux-optimism increases the size of your store. With short optimistic durations and only little optimistic actions it's about double the size of your realistic store. In other cases it's going to be more (depends on the size of your actions).
  • You have to take care, that the user can't take actions that depend on an optimistic update.

Usage with combineReducers

CombineReducers doesn't like keys other than it's own. If you just wrap combineReducers, redux-optimism won't work. Thus we have to add the key:

import {combineReducers} from 'redux'
import {user, userInterface} from "store/user/reducers"

function optimism(state: any = {}, action: any) {
    return state
}

const rootReducer = combineReducers({
    user,
    userInterface,
    optimism
})

export {rootReducer}

Check for optimistic actions in your own reducers

Optimistic actions are marked with action.meta.optimisticId = ID. But you can also just

import {isOptimistic} from 'redux-optimism'

function reducer(state: any, action: any) {
    if (isOptimistic(action)) {
        ...
    }
}

What does getOptimism do?

getOptimism is a really simple function: It generates a new optimisticId, dispatches

{
    type: 'INIT_OPTIMISM',
    payload: {optimisticId: optimisticId}
}

and returns the three functions optimistic(action), commit() and rollback(). Commit and rollback are just

dispatch({
    type: 'COMMIT_OPTIMISM',
    payload: {optimisticId: optimisticId}
})

and

dispatch({
    type: 'ROLLBACK_OPTIMISM',
    payload: {optimisticId: optimisticId}
})

while optimistic(action) dispatches the given action with action.meta.optimisticId set:

dispatch({
    ...action,
    meta: {optimisticId: optimisticId}
})

So, you could use the complete functionality of redux-optimism without getOptimism. However, I recommend using getOptimism, as it's a bit more opinionated:

If an optimism was initialized and neither committed or rolled back after 20 seconds, it will log an error and will automatically get rolled back. Why is this important? If you don't commit or rollback an optimism for a longer time, your state can be hard to understand later. You should try to make the state as deterministic as possible. Also the store grows in size if an optimistic action is pending for a longer time.

import { createStore, applyMiddleware } from 'redux';
import {makeOptimistic, getOptimism} from 'redux-optimism'


function rootReducer(previousState: any = {}, action: any) {
    if (action.type === 'INCREMENT') {
        return {
            ...previousState,
            test: 1
        }
    } else {
        return previousState
    }
}

const store = createStore(
    makeOptimistic(rootReducer)
)

let {optimistic, commit, rollback} = getOptimism(store.dispatch)
optimistic({type: 'INCREMENT'})

console.log(store.getState().test)
// Logs 1

// Wait for 20s

console.log(store.getState().test)
// Logs undefined

You can change the behavior easily: getOptimism takes more arguments:

let {optimistic, commit, rollback} = getOptimism(
    store.dispatch, 
    60000, // Set the timeout to 60s
    (optimisticId, commit, rollback) => {commit()}) // Commit in case of a timeout

Or, if you want to use another default behaviour

import {setDefaultTimeout, setDefaultTimeoutHandler} from 'redux-optimism'

setDefaultTimeout(10000) // Use 10s as the default timeout
setDefaultTimeoutHandler((optimisticId, commit, rollback) => {
    console.log("I don't care")
})

Why did I write this package?

There are other optimistic state enhancers:

I wanted an intersection of both of them with a bit on top:

  • It should only use one copy of the state (redux-optimistic-ui)
  • FSA compliance (redux-optimistic-ui)
  • No need to change the structure of the application state (redux-optimist — redux-optimistic-ui requires you to wrap getState() every single time with ensureState()).
  • Don't care about IDs (none of them)
  • Provide types (none of them)

Additionally, redux-optimist has some issues and pull requests that are open several years. So I wrote this package. But keep in mind: This is in alpha stage!

License

MIT