persistReducer.js.flow 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. // @flow
  2. import {
  3. FLUSH,
  4. PAUSE,
  5. PERSIST,
  6. PURGE,
  7. REHYDRATE,
  8. DEFAULT_VERSION,
  9. } from './constants'
  10. import type {
  11. PersistConfig,
  12. MigrationManifest,
  13. PersistState,
  14. Persistoid,
  15. } from './types'
  16. import autoMergeLevel1 from './stateReconciler/autoMergeLevel1'
  17. import createPersistoid from './createPersistoid'
  18. import defaultGetStoredState from './getStoredState'
  19. import purgeStoredState from './purgeStoredState'
  20. type PersistPartial = { _persist: PersistState }
  21. const DEFAULT_TIMEOUT = 5000
  22. /*
  23. @TODO add validation / handling for:
  24. - persisting a reducer which has nested _persist
  25. - handling actions that fire before reydrate is called
  26. */
  27. export default function persistReducer<State: Object, Action: Object>(
  28. config: PersistConfig,
  29. baseReducer: (State, Action) => State
  30. ): (State, Action) => State & PersistPartial {
  31. if (process.env.NODE_ENV !== 'production') {
  32. if (!config) throw new Error('config is required for persistReducer')
  33. if (!config.key) throw new Error('key is required in persistor config')
  34. if (!config.storage)
  35. throw new Error(
  36. "redux-persist: config.storage is required. Try using one of the provided storage engines `import storage from 'redux-persist/lib/storage'`"
  37. )
  38. }
  39. const version =
  40. config.version !== undefined ? config.version : DEFAULT_VERSION
  41. const debug = config.debug || false
  42. const stateReconciler =
  43. config.stateReconciler === undefined
  44. ? autoMergeLevel1
  45. : config.stateReconciler
  46. const getStoredState = config.getStoredState || defaultGetStoredState
  47. const timeout =
  48. config.timeout !== undefined ? config.timeout : DEFAULT_TIMEOUT
  49. let _persistoid = null
  50. let _purge = false
  51. let _paused = true
  52. const conditionalUpdate = state => {
  53. // update the persistoid only if we are rehydrated and not paused
  54. state._persist.rehydrated &&
  55. _persistoid &&
  56. !_paused &&
  57. _persistoid.update(state)
  58. return state
  59. }
  60. return (state: State, action: Action) => {
  61. let { _persist, ...rest } = state || {}
  62. // $FlowIgnore need to update State type
  63. let restState: State = rest
  64. if (action.type === PERSIST) {
  65. let _sealed = false
  66. let _rehydrate = (payload, err) => {
  67. // dev warning if we are already sealed
  68. if (process.env.NODE_ENV !== 'production' && _sealed)
  69. console.error(
  70. `redux-persist: rehydrate for "${
  71. config.key
  72. }" called after timeout.`,
  73. payload,
  74. err
  75. )
  76. // only rehydrate if we are not already sealed
  77. if (!_sealed) {
  78. action.rehydrate(config.key, payload, err)
  79. _sealed = true
  80. }
  81. }
  82. timeout &&
  83. setTimeout(() => {
  84. !_sealed &&
  85. _rehydrate(
  86. undefined,
  87. new Error(
  88. `redux-persist: persist timed out for persist key "${
  89. config.key
  90. }"`
  91. )
  92. )
  93. }, timeout)
  94. // @NOTE PERSIST resumes if paused.
  95. _paused = false
  96. // @NOTE only ever create persistoid once, ensure we call it at least once, even if _persist has already been set
  97. if (!_persistoid) _persistoid = createPersistoid(config)
  98. // @NOTE PERSIST can be called multiple times, noop after the first
  99. if (_persist) {
  100. // We still need to call the base reducer because there might be nested
  101. // uses of persistReducer which need to be aware of the PERSIST action
  102. return {
  103. ...baseReducer(restState, action),
  104. _persist,
  105. };
  106. }
  107. if (
  108. typeof action.rehydrate !== 'function' ||
  109. typeof action.register !== 'function'
  110. )
  111. throw new Error(
  112. '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.'
  113. )
  114. action.register(config.key)
  115. getStoredState(config).then(
  116. restoredState => {
  117. const migrate = config.migrate || ((s, v) => Promise.resolve(s))
  118. migrate(restoredState, version).then(
  119. migratedState => {
  120. _rehydrate(migratedState)
  121. },
  122. migrateErr => {
  123. if (process.env.NODE_ENV !== 'production' && migrateErr)
  124. console.error('redux-persist: migration error', migrateErr)
  125. _rehydrate(undefined, migrateErr)
  126. }
  127. )
  128. },
  129. err => {
  130. _rehydrate(undefined, err)
  131. }
  132. )
  133. return {
  134. ...baseReducer(restState, action),
  135. _persist: { version, rehydrated: false },
  136. }
  137. } else if (action.type === PURGE) {
  138. _purge = true
  139. action.result(purgeStoredState(config))
  140. return {
  141. ...baseReducer(restState, action),
  142. _persist,
  143. }
  144. } else if (action.type === FLUSH) {
  145. action.result(_persistoid && _persistoid.flush())
  146. return {
  147. ...baseReducer(restState, action),
  148. _persist,
  149. }
  150. } else if (action.type === PAUSE) {
  151. _paused = true
  152. } else if (action.type === REHYDRATE) {
  153. // noop on restState if purging
  154. if (_purge)
  155. return {
  156. ...restState,
  157. _persist: { ..._persist, rehydrated: true },
  158. }
  159. // @NOTE if key does not match, will continue to default else below
  160. if (action.key === config.key) {
  161. let reducedState = baseReducer(restState, action)
  162. let inboundState = action.payload
  163. // only reconcile state if stateReconciler and inboundState are both defined
  164. let reconciledRest: State =
  165. stateReconciler !== false && inboundState !== undefined
  166. ? stateReconciler(inboundState, state, reducedState, config)
  167. : reducedState
  168. let newState = {
  169. ...reconciledRest,
  170. _persist: { ..._persist, rehydrated: true },
  171. }
  172. return conditionalUpdate(newState)
  173. }
  174. }
  175. // if we have not already handled PERSIST, straight passthrough
  176. if (!_persist) return baseReducer(state, action)
  177. // run base reducer:
  178. // is state modified ? return original : return updated
  179. let newState = baseReducer(restState, action)
  180. if (newState === restState) return state
  181. return conditionalUpdate({ ...newState, _persist })
  182. }
  183. }