Skip to content

Instantly share code, notes, and snippets.

@ikhsanalatsary
Created March 22, 2018 05:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ikhsanalatsary/2fb87c7f2ed8419995da7fe97d26dfba to your computer and use it in GitHub Desktop.
Save ikhsanalatsary/2fb87c7f2ed8419995da7fe97d26dfba to your computer and use it in GitHub Desktop.
redux-persist persistReducer with seamless-immutable
// @flow
import {
FLUSH,
PAUSE,
PERSIST,
PURGE,
REHYDRATE,
DEFAULT_VERSION
} from 'redux-persist'
import Immutable from 'seamless-immutable'
import { equals } from 'ramda'
import type {
PersistConfig,
MigrationManifest,
PersistState,
Persistoid
} from 'redux-persist/lib/types'
import autoMergeLevel1 from 'redux-persist/lib/stateReconciler/autoMergeLevel1'
import createPersistoid from 'redux-persist/lib/createPersistoid'
import defaultGetStoredState from 'redux-persist/lib/getStoredState'
import purgeStoredState from 'redux-persist/lib/purgeStoredState'
type PersistPartial = { _persist: PersistState }
const DEFAULT_TIMEOUT = 5000
/*
@TODO add validation / handling for:
- persisting a reducer which has nested _persist
- handling actions that fire before reydrate is called
*/
export default function persistReducer<State: Object, Action: Object> (
config: PersistConfig,
baseReducer: (State, Action) => State
): (State, Action) => State & PersistPartial {
if (process.env.NODE_ENV !== 'production') {
if (!config) throw new Error('config is required for persistReducer')
if (!config.key) throw new Error('key is required in persistor config')
if (!config.storage) {
throw new Error(
"redux-persist: config.storage is required. Try using one of the provided storage engines `import storageLocal from 'redux-persist/es/storage/local'"
)
}
}
const version =
config.version !== undefined ? config.version : DEFAULT_VERSION
const debug = config.debug || false
const stateReconciler =
config.stateReconciler === undefined
? autoMergeLevel1
: config.stateReconciler
const getStoredState = config.getStoredState || defaultGetStoredState
const timeout =
config.timeout !== undefined ? config.timeout : DEFAULT_TIMEOUT
let _persistoid = null
let _purge = false
let _paused = true
const conditionalUpdate = state => {
// update the persistoid only if we are rehydrated and not paused
state._persist.rehydrated &&
_persistoid &&
!_paused &&
_persistoid.update(state)
return state
}
return (state: State, action: Action) => {
let { _persist, ...rest } = state || Immutable({})
let restState: State = Immutable(rest)
if (action.type === PERSIST) {
let _sealed = false
let _rehydrate = (payload, err) => {
// dev warning if we are already sealed
if (process.env.NODE_ENV !== 'production' && _sealed) {
console.error(
`redux-persist: rehydrate for "${
config.key
}" called after timeout.`,
payload,
err
)
}
// only rehydrate if we are not already sealed
if (!_sealed) {
action.rehydrate(config.key, payload, err)
_sealed = true
}
}
timeout &&
setTimeout(() => {
!_sealed &&
_rehydrate(
undefined,
new Error(
`redux-persist: persist timed out for persist key "${
config.key
}"`
)
)
}, timeout)
// @NOTE PERSIST resumes if paused.
_paused = false
// @NOTE only ever create persistoid once, ensure we call it at least once, even if _persist has already been set
if (!_persistoid) _persistoid = createPersistoid(config)
// @NOTE PERSIST can be called multiple times, noop after the first
if (_persist) return state
if (
typeof action.rehydrate !== 'function' ||
typeof action.register !== 'function'
) {
throw new Error(
'redux-persist: either rehydrate or register is not a function on the PERSIST action. This can happen if the action is being replayed. This is an unexplored use case, please open an issue and we will figure out a resolution.'
)
}
action.register(config.key)
getStoredState(config).then(
restoredState => {
const migrate = config.migrate || ((s, v) => Promise.resolve(s))
migrate(restoredState, version).then(
migratedState => {
_rehydrate(migratedState)
},
migrateErr => {
if (process.env.NODE_ENV !== 'production' && migrateErr) { console.error('redux-persist: migration error', migrateErr) }
_rehydrate(undefined, migrateErr)
}
)
},
err => {
_rehydrate(undefined, err)
}
)
return Immutable({
...baseReducer(restState, action),
_persist: { version, rehydrated: false }
})
} else if (action.type === PURGE) {
_purge = true
action.result(purgeStoredState(config))
return Immutable({
...baseReducer(restState, action),
_persist
})
} else if (action.type === FLUSH) {
action.result(_persistoid && _persistoid.flush())
return Immutable({
...baseReducer(restState, action),
_persist
})
} else if (action.type === PAUSE) {
_paused = true
} else if (action.type === REHYDRATE) {
// noop on restState if purging
if (_purge) {
return Immutable({
...restState,
_persist: { ..._persist, rehydrated: true }
})
}
// @NOTE if key does not match, will continue to default else below
if (action.key === config.key) {
let reducedState = baseReducer(restState, action)
let inboundState = action.payload
// only reconcile state if stateReconciler and inboundState are both defined
let reconciledRest: State =
stateReconciler !== false && inboundState !== undefined
? stateReconciler(inboundState, state, reducedState, config)
: reducedState
let newState = Immutable({
...reconciledRest,
_persist: { ..._persist, rehydrated: true }
})
return conditionalUpdate(newState)
}
}
// if we have not already handled PERSIST, straight passthrough
if (!_persist) {
return baseReducer(state, action)
}
// run base reducer:
// is state modified ? return original : return updated
let newState = baseReducer(restState, action)
if (equals(newState.asMutable({ deep: true }), restState.asMutable({ deep: true }))) {
return state
} else {
newState = Immutable({
...newState,
_persist
})
return conditionalUpdate(newState)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment