├── .babelrc.js ├── .eslintrc.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LIBSIZE.md ├── LICENSE ├── README.md ├── docs ├── MigrationGuide-v5.md ├── PersistGate.md ├── api.md ├── hot-module-replacement.md ├── migrations.md └── v5-migration-alternate.md ├── integration ├── README.md └── react │ └── package.json ├── package-lock.json ├── package.json ├── rollup.config.js ├── scripts └── size-estimator.js ├── src ├── constants.ts ├── createMigrate.ts ├── createPersistoid.ts ├── createTransform.ts ├── getStoredState.ts ├── index.ts ├── integration │ ├── getStoredStateMigrateV4.ts │ └── react.ts ├── persistCombineReducers.ts ├── persistReducer.ts ├── persistStore.ts ├── purgeStoredState.ts ├── stateReconciler │ ├── autoMergeLevel1.ts │ ├── autoMergeLevel2.ts │ └── hardSet.ts ├── storage │ ├── createWebStorage.ts │ ├── getStorage.ts │ ├── index.ts │ └── session.ts └── types.ts ├── tests ├── complete.spec.ts ├── createPersistor.spec.ts ├── flush.spec.ts ├── persistCombineReducers.spec.ts ├── persistReducer.spec.ts ├── persistStore.spec.ts └── utils │ ├── brokenStorage.ts │ ├── createMemoryStorage.ts │ ├── find.ts │ └── sleep.ts ├── tsconfig.json └── types └── types.d.ts /.babelrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Babel Configuration 3 | */ 4 | module.exports = { 5 | presets: [ 6 | [ 7 | "@babel/preset-env" 8 | ], 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | parser: '@typescript-eslint/parser' 4 | 5 | globals: 6 | it: true 7 | expect: true 8 | describe: true 9 | test: true 10 | jest: true 11 | 12 | plugins: 13 | - '@typescript-eslint' 14 | 15 | extends: 16 | - eslint:recommended 17 | - plugin:@typescript-eslint/recommended 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: redux-persist ci 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | # Label of the container job 6 | container-job: 7 | # Containers must run in Linux based operating systems 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install 20 | run: | 21 | npm install 22 | - name: run test 23 | run: | 24 | npm run test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | es 2 | lib 3 | dist 4 | types 5 | .watchmanconfig 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | 12 | # Dependency directories 13 | node_modules 14 | 15 | # Optional npm cache directory 16 | .npm 17 | 18 | # Optional REPL history 19 | .node_repl_history 20 | 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | parser: "typescript" 2 | semi: false 3 | trailingComma: es5 4 | singleQuote: true 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project (after v6.1.0) should be documented in this file. 3 | 4 | The format is (mostly) based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [6.1.0] - 2021-10-17 9 | Thanks to [@smellman](https://github.com/smellman) for the TypeScript updates. 10 | 11 | ### Added 12 | - TypeScript support 13 | - GitHub Actions 14 | 15 | ### Changed 16 | - Move from Flow to TypeScript 17 | - Move from TravisCI to GitHub Actions ([.github/workflows/ci.yml](.github/workflows/ci.yml)) 18 | - Version updates for some dependencies 19 | 20 | ### Removed 21 | - Flow 22 | - TravisCI 23 | -------------------------------------------------------------------------------- /LIBSIZE.md: -------------------------------------------------------------------------------- 1 | ### Redux Persist Size Estimate 2 | The following is a history of size estimates in bytes. This is calculated as a rollup minified production build, excluding the size of redux which is an assumed peer dependency. YMMV. 3 | 4 | **v5.6.7**: 4724 Bytes 5 | **v5.6.8**: 4724 Bytes 6 | **v5.6.9**: 4724 Bytes 7 | **v5.6.10**: 4724 Bytes 8 | **v5.6.11**: 4724 Bytes 9 | **v5.6.12**: 4724 Bytes 10 | **v5.7.0**: 4893 Bytes 11 | **v5.7.1**: 4894 Bytes 12 | **v5.7.2**: 4894 Bytes 13 | **v5.8.0**: 4894 Bytes 14 | **v5.9.0**: 4894 Bytes 15 | **v5.9.1**: 4894 Bytes 16 | **v5.10.0**: 4356 Bytes 17 | **v6.0.0-pre1**: 17783 Bytes 18 | **v6.0.0-pre2**: 11878 Bytes 19 | **v6.0.0-pre2.0**: 11934 Bytes 20 | **v6.0.0-pre2.1**: 5525 Bytes 21 | **v6.0.0**: 12167 Bytes 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zack Story 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux Persist 2 | Persist and rehydrate a redux store. 3 | 4 | [![build status](https://img.shields.io/travis/rt2zz/redux-persist/master.svg?style=flat-square)](https://travis-ci.org/rt2zz/redux-persist) [![npm version](https://img.shields.io/npm/v/redux-persist.svg?style=flat-square)](https://www.npmjs.com/package/redux-persist) [![npm downloads](https://img.shields.io/npm/dm/redux-persist.svg?style=flat-square)](https://www.npmjs.com/package/redux-persist) 5 | 6 | ## October 15th, 2021 - Move to TypeScript (Thanks [@smellman](https://github.com/smellman)) 7 | 8 | As part of the work to upgrade the infrastructure used to build redux-persist, we're moving from Flow to TypeScript. 9 | 10 | - Move from Flow to TypeScript 11 | - Move from TravisCI to GitHub Actions ([.github/workflows/ci.yml](.github/workflows/ci.yml)) 12 | - Version updates for some dependencies 13 | 14 | ## September 22nd, 2021 - Under New Management 15 | 16 | Redux Persist is a staple project for Redux developers, both on mobile and on the web. If you're here, it's likely because you need it now or have used it before and need to debug something, and like me have possibly struggled with making it work (especially with newer versions of things) and making it work with _your_ code because the examples you'll find around the internet are inconsistent. 17 | 18 | I ([@ckalika](https://github.com/ckalika)) spoke with [@rt2zz](https://github.com/rt2zz) about taking over maintenance of the project, and we agreed to give it a shot and see how we go. My priorities are as follows: 19 | 20 | 1. Go through and triage the existing issues 21 | - Separate them into bugs, feature requests, basic questions/requests for code samples, and issues that are either not project-specific or don't fall within the remit of the project (specific definitions and criteria will be posted in the future) 22 | - Determine the severity/urgency of each bug or feature request 23 | - Guestimate the size of them 24 | - Determine which are actionable immediately or in the short term 25 | - Establish some semblance of test criteria for each 26 | 27 | 28 | 2. Upgrade dependencies (where possible) so that we've got something building with modern versions 29 | * Note: Right now, it's about modernising the project infrastructure and build process without making breaking API changes 30 | 31 | 32 | 3. Go through the existing pull requests 33 | - Merge the ones that deal with documentation, code samples, etc. 34 | - Review and merge the ones that deal with open issues 35 | - Review and merge the ones that will require breaking changes and consult authors about `redux-persist@v7` (feature set and requirements to be defined) 36 | 37 | 38 | 4. Update the documentation 39 | - Split it out for both web and mobile 40 | - Providing code samples and test coverage for how to use the library 41 | - Provide or link to working examples that integrate with additional libraries (e.g. [RTK Query](https://redux-toolkit.js.org/rtk-query/overview)). 42 | 43 | 44 | 5. Improve testing and automation 45 | - [x] Move to GitHub Actions 46 | - [ ] Move from Ava to Jest 47 | 48 | There's a lot to do here, so I'll ask your patience and understanding as I work through it. If you have ideas for how to improve the library, the documentation, or the community, I'd love to hear them, and if you're submitting pull requests (or have submitted some previously), please reach out and help me understand what you're aiming to do with it. 49 | 50 | I'll try to get some discussions up to pull together ideas, so we can properly work out what the next version is likely to look like. 51 | 52 | 53 | ## v6 upgrade 54 | **Web**: no breaking changes 55 | **React Native**: Users must now explicitly pass their storage engine in. e.g. 56 | ```js 57 | import AsyncStorage from '@react-native-async-storage/async-storage'; 58 | 59 | const persistConfig = { 60 | //... 61 | storage: AsyncStorage 62 | } 63 | ``` 64 | 65 | ## Quickstart 66 | `npm install redux-persist` 67 | 68 | Usage Examples: 69 | 1. [Basic Usage](#basic-usage) 70 | 2. [Nested Persists](#nested-persists) 71 | 3. [Hot Module Replacement](./docs/hot-module-replacement.md) 72 | 4. Code Splitting [coming soon] 73 | 74 | #### Basic Usage 75 | Basic usage involves adding `persistReducer` and `persistStore` to your setup. **IMPORTANT** Every app needs to decide how many levels of state they want to "merge". The default is 1 level. Please read through the [state reconciler docs](#state-reconciler) for more information. 76 | 77 | ```js 78 | // configureStore.js 79 | 80 | import { createStore } from 'redux' 81 | import { persistStore, persistReducer } from 'redux-persist' 82 | import storage from 'redux-persist/lib/storage' // defaults to localStorage for web 83 | 84 | import rootReducer from './reducers' 85 | 86 | const persistConfig = { 87 | key: 'root', 88 | storage, 89 | } 90 | 91 | const persistedReducer = persistReducer(persistConfig, rootReducer) 92 | 93 | export default () => { 94 | let store = createStore(persistedReducer) 95 | let persistor = persistStore(store) 96 | return { store, persistor } 97 | } 98 | ``` 99 | 100 | If you are using react, wrap your root component with [PersistGate](./docs/PersistGate.md). This delays the rendering of your app's UI until your persisted state has been retrieved and saved to redux. **NOTE** the `PersistGate` loading prop can be null, or any react instance, e.g. `loading={}` 101 | 102 | ```js 103 | import { PersistGate } from 'redux-persist/integration/react' 104 | 105 | // ... normal setup, create store and persistor, import components etc. 106 | 107 | const App = () => { 108 | return ( 109 | 110 | 111 | 112 | 113 | 114 | ); 115 | }; 116 | ``` 117 | 118 | ## API 119 | [Full API](./docs/api.md) 120 | 121 | #### `persistReducer(config, reducer)` 122 | - arguments 123 | - [**config**](https://github.com/rt2zz/redux-persist/blob/master/src/types.js#L13-L27) *object* 124 | - required config: `key, storage` 125 | - notable other config: `whitelist, blacklist, version, stateReconciler, debug` 126 | - **reducer** *function* 127 | - any reducer will work, typically this would be the top level reducer returned by `combineReducers` 128 | - returns an enhanced reducer 129 | 130 | #### `persistStore(store, [config, callback])` 131 | - arguments 132 | - **store** *redux store* The store to be persisted. 133 | - **config** *object* (typically null) 134 | - If you want to avoid that the persistence starts immediately after calling `persistStore`, set the option manualPersist. Example: `{ manualPersist: true }` Persistence can then be started at any point with `persistor.persist()`. You usually want to do this if your storage is not ready when the `persistStore` call is made. 135 | - **callback** *function* will be called after rehydration is finished. 136 | - returns **persistor** object 137 | 138 | #### `persistor object` 139 | - the persistor object is returned by persistStore with the following methods: 140 | - `.purge()` 141 | - purges state from disk and returns a promise 142 | - `.flush()` 143 | - immediately writes all pending state to disk and returns a promise 144 | - `.pause()` 145 | - pauses persistence 146 | - `.persist()` 147 | - resumes persistence 148 | 149 | ## State Reconciler 150 | State reconcilers define how incoming state is merged in with initial state. It is critical to choose the right state reconciler for your state. There are three options that ship out of the box, let's look at how each operates: 151 | 152 | 1. **hardSet** (`import hardSet from 'redux-persist/lib/stateReconciler/hardSet'`) 153 | This will hard set incoming state. This can be desirable in some cases where persistReducer is nested deeper in your reducer tree, or if you do not rely on initialState in your reducer. 154 | - **incoming state**: `{ foo: incomingFoo }` 155 | - **initial state**: `{ foo: initialFoo, bar: initialBar }` 156 | - **reconciled state**: `{ foo: incomingFoo }` // note bar has been dropped 157 | 2. **autoMergeLevel1** (default) 158 | This will auto merge one level deep. Auto merge means if the some piece of substate was modified by your reducer during the REHYDRATE action, it will skip this piece of state. Level 1 means it will shallow merge 1 level deep. 159 | - **incoming state**: `{ foo: incomingFoo }` 160 | - **initial state**: `{ foo: initialFoo, bar: initialBar }` 161 | - **reconciled state**: `{ foo: incomingFoo, bar: initialBar }` // note incomingFoo overwrites initialFoo 162 | 3. **autoMergeLevel2** (`import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'`) 163 | This acts just like autoMergeLevel1, except it shallow merges two levels 164 | - **incoming state**: `{ foo: incomingFoo }` 165 | - **initial state**: `{ foo: initialFoo, bar: initialBar }` 166 | - **reconciled state**: `{ foo: mergedFoo, bar: initialBar }` // note: initialFoo and incomingFoo are shallow merged 167 | 168 | #### Example 169 | ```js 170 | import hardSet from 'redux-persist/lib/stateReconciler/hardSet' 171 | 172 | const persistConfig = { 173 | key: 'root', 174 | storage, 175 | stateReconciler: hardSet, 176 | } 177 | ``` 178 | 179 | ## React Integration 180 | Redux persist ships with react integration as a convenience. The `PersistGate` component is the recommended way to delay rendering until persistence is complete. It works in one of two modes: 181 | 1. `loading` prop: The provided loading value will be rendered until persistence is complete at which point children will be rendered. 182 | 2. function children: The function will be invoked with a single `bootstrapped` argument. When bootstrapped is true, persistence is complete and it is safe to render the full app. This can be useful for adding transition animations. 183 | 184 | ## Blacklist & Whitelist 185 | By Example: 186 | ```js 187 | // BLACKLIST 188 | const persistConfig = { 189 | key: 'root', 190 | storage: storage, 191 | blacklist: ['navigation'] // navigation will not be persisted 192 | }; 193 | 194 | // WHITELIST 195 | const persistConfig = { 196 | key: 'root', 197 | storage: storage, 198 | whitelist: ['navigation'] // only navigation will be persisted 199 | }; 200 | ``` 201 | 202 | ## Nested Persists 203 | Nested persist can be useful for including different storage adapters, code splitting, or deep filtering. For example while blacklist and whitelist only work one level deep, but we can use a nested persist to blacklist a deeper value: 204 | ```js 205 | import { combineReducers } from 'redux' 206 | import { persistReducer } from 'redux-persist' 207 | import storage from 'redux-persist/lib/storage' 208 | 209 | import { authReducer, otherReducer } from './reducers' 210 | 211 | const rootPersistConfig = { 212 | key: 'root', 213 | storage: storage, 214 | blacklist: ['auth'] 215 | } 216 | 217 | const authPersistConfig = { 218 | key: 'auth', 219 | storage: storage, 220 | blacklist: ['somethingTemporary'] 221 | } 222 | 223 | const rootReducer = combineReducers({ 224 | auth: persistReducer(authPersistConfig, authReducer), 225 | other: otherReducer, 226 | }) 227 | 228 | export default persistReducer(rootPersistConfig, rootReducer) 229 | ``` 230 | 231 | ## Migrations 232 | `persistReducer` has a general purpose "migrate" config which will be called after getting stored state but before actually reconciling with the reducer. It can be any function which takes state as an argument and returns a promise to return a new state object. 233 | 234 | Redux Persist ships with `createMigrate`, which helps create a synchronous migration for moving from any version of stored state to the current state version. [[Additional information]](./docs/migrations.md) 235 | 236 | ## Transforms 237 | Transforms allow you to customize the state object that gets persisted and rehydrated. 238 | 239 | There are several libraries that tackle some common implementations for transforms. 240 | - [immutable](https://github.com/rt2zz/redux-persist-transform-immutable) - support immutable reducers 241 | - [seamless-immutable](https://github.com/hilkeheremans/redux-persist-seamless-immutable) - support seamless-immutable reducers 242 | - [compress](https://github.com/rt2zz/redux-persist-transform-compress) - compress your serialized state with lz-string 243 | - [encrypt](https://github.com/maxdeviant/redux-persist-transform-encrypt) - encrypt your serialized state with AES 244 | - [filter](https://github.com/edy/redux-persist-transform-filter) - store or load a subset of your state 245 | - [filter-immutable](https://github.com/actra-development/redux-persist-transform-filter-immutable) - store or load a subset of your state with support for immutablejs 246 | - [expire](https://github.com/gabceb/redux-persist-transform-expire) - expire a specific subset of your state based on a property 247 | - [expire-reducer](https://github.com/kamranahmedse/redux-persist-expire) - more flexible alternative to expire transformer above with more options 248 | 249 | When the state object gets persisted, it first gets serialized with `JSON.stringify()`. If parts of your state object are not mappable to JSON objects, the serialization process may transform these parts of your state in unexpected ways. For example, the javascript [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) type does not exist in JSON. When you try to serialize a Set via `JSON.stringify()`, it gets converted to an empty object. Probably not what you want. 250 | 251 | Below is a Transform that successfully persists a Set property, which simply converts it to an array and back. In this way, the Set gets converted to an Array, which is a recognized data structure in JSON. When pulled out of the persisted store, the array gets converted back to a Set before being saved to the redux store. 252 | 253 | ```js 254 | import { createTransform } from 'redux-persist'; 255 | 256 | const SetTransform = createTransform( 257 | // transform state on its way to being serialized and persisted. 258 | (inboundState, key) => { 259 | // convert mySet to an Array. 260 | return { ...inboundState, mySet: [...inboundState.mySet] }; 261 | }, 262 | // transform state being rehydrated 263 | (outboundState, key) => { 264 | // convert mySet back to a Set. 265 | return { ...outboundState, mySet: new Set(outboundState.mySet) }; 266 | }, 267 | // define which reducers this transform gets called for. 268 | { whitelist: ['someReducer'] } 269 | ); 270 | 271 | export default SetTransform; 272 | ``` 273 | 274 | The `createTransform` function takes three parameters. 275 | 1. An "inbound" function that gets called right before state is persisted (optional). 276 | 2. An "outbound" function that gets called right before state is rehydrated (optional). 277 | 3. A config object that determines which keys in your state will be transformed (by default no keys are transformed). 278 | 279 | In order to take effect transforms need to be added to a `PersistReducer`’s config object. 280 | 281 | ``` 282 | import storage from 'redux-persist/lib/storage'; 283 | import { SetTransform } from './transforms'; 284 | 285 | const persistConfig = { 286 | key: 'root', 287 | storage: storage, 288 | transforms: [SetTransform] 289 | }; 290 | ``` 291 | 292 | ## Storage Engines 293 | - **localStorage** `import storage from 'redux-persist/lib/storage'` 294 | - **sessionStorage** `import storageSession from 'redux-persist/lib/storage/session'` 295 | - **[electron storage](https://github.com/psperber/redux-persist-electron-storage)** Electron support via [electron store](https://github.com/sindresorhus/electron-store) 296 | - **[redux-persist-cookie-storage](https://github.com/abersager/redux-persist-cookie-storage)** Cookie storage engine, works in browser and Node.js, for universal / isomorphic apps 297 | - **[redux-persist-expo-filesystem](https://github.com/t73liu/redux-persist-expo-filesystem)** react-native, similar to redux-persist-filesystem-storage but does not require linking or ejecting CRNA/Expo app. Only available if using Expo SDK (Expo, create-react-native-app, standalone). 298 | - **[redux-persist-expo-securestore](https://github.com/Cretezy/redux-persist-expo-securestore)** react-native, for sensitive information using Expo's SecureStore. Only available if using Expo SDK (Expo, create-react-native-app, standalone). 299 | - **[redux-persist-fs-storage](https://github.com/leethree/redux-persist-fs-storage)** react-native-fs engine 300 | - **[redux-persist-filesystem-storage](https://github.com/robwalkerco/redux-persist-filesystem-storage)** react-native, to mitigate storage size limitations in android ([#199](https://github.com/rt2zz/redux-persist/issues/199), [#284](https://github.com/rt2zz/redux-persist/issues/284)) 301 | **[redux-persist-indexeddb-storage](https://github.com/machester4/redux-persist-indexeddb-storage)** recommended for web via [localForage](https://github.com/localForage/localForage) 302 | - **[redux-persist-node-storage](https://github.com/pellejacobs/redux-persist-node-storage)** for use in nodejs environments. 303 | - **[redux-persist-pouchdb](https://github.com/yanick/redux-persist-pouchdb)** Storage engine for PouchDB. 304 | - **[redux-persist-sensitive-storage](https://github.com/CodingZeal/redux-persist-sensitive-storage)** react-native, for sensitive information (uses [react-native-sensitive-info](https://github.com/mCodex/react-native-sensitive-info)). 305 | - **[redux-persist-weapp-storage](https://github.com/cuijiemmx/redux-casa/tree/master/packages/redux-persist-weapp-storage)** Storage engine for wechat mini program, also compatible with wepy 306 | - **[redux-persist-webextension-storage](https://github.com/ssorallen/redux-persist-webextension-storage)** Storage engine for browser (Chrome, Firefox) web extension storage 307 | - **[@bankify/redux-persist-realm](https://github.com/bankifyio/redux-persist-realm)** Storage engine for Realm database, you will need to install Realm first 308 | - **custom** any conforming storage api implementing the following methods: `setItem` `getItem` `removeItem`. (**NB**: These methods must support promises) 309 | 310 | ## Community & Contributing 311 | 312 | I will be updating this section shortly. If you have a pull request that you've got outstanding, please reach out and I will try to review it and get it integrated. As we've shifted to TypeScript, that may necessitate some changes, but I'm happy to help in that regard, wherever I can. 313 | -------------------------------------------------------------------------------- /docs/MigrationGuide-v5.md: -------------------------------------------------------------------------------- 1 | ## v5 Breaking Changes 2 | There are three important breaking changes. 3 | 1. api has changed as described in the [migration](#migration-from-v4-to-v5) section below. 4 | 2. state with cycles is no longer serialized using `json-stringify-safe`, and will instead noop. 5 | 3. state methods can no longer be overridden which means all top level state needs to be plain objects. `redux-persist-transform-immutable` will continue to operate as before as it works on substate, not top level state. 6 | 7 | Additionally v5 does not yet have typescript bindings. 8 | 9 | ## Migration from v4 to v5 10 | **WARNING** v4 stored state is not compatible with v5. If you upgrade a v4 application, your users will lose their stored state upon upgrade. You can try the (highly) experimental [v4 -> v5 state migration](#experimental-v4-to-v5-state-migration) if you please. Feedback appreciated. 11 | 12 | Standard Usage: 13 | - remove **autoRehydrate** 14 | - changes to **persistStore**: 15 |  - 1. remove config argument (or replace with an null if you are using a callback) 16 | - 2. remove all arguments from the callback. If you need state you can call `store.getState()` 17 | - 3. all constants (ex: `REHYDRATE`, `PURGE`) has moved from `redux-persist/constants` to the root module. 18 | - replace `combineReducers` with **persistCombineReducers** 19 | - e.g. `let reducer = persistCombineReducers(config, reducers)` 20 | - changes to **config**: 21 | - `key` is now required. Can be set to anything, e.g. 'primary' 22 | - `storage` is now required. For default storage: `import storage from 'redux-persist/lib/storage'` 23 | 24 | ```diff 25 | -import { REHYDRATE, PURGE } from 'redux-persist/constants' 26 | -import { combineReducers } from 'redux' 27 | +import { REHYDRATE, PURGE, persistCombineReducers } from 'redux-persist' 28 | +import storage from 'redux-persist/lib/storage' // or whatever storage you are using 29 | 30 | const config = { 31 | + key: 'primary', 32 | + storage 33 | } 34 | 35 | -let reducer = combineReducers(reducers) 36 | +let reducer = persistCombineReducers(config, reducers) 37 | 38 | const store = createStore( 39 | reducer, 40 | undefined, 41 | compose( 42 | applyMiddleware(...), 43 | - autoRehydrate() 44 | ) 45 | ) 46 | 47 | const callback = () 48 | 49 | persistStore( 50 | store, 51 | - config, 52 | + null, 53 | ( 54 | - err, restoredState 55 | ) => { 56 | + store.getState() // if you want to get restoredState 57 | } 58 | ) 59 | ``` 60 | 61 | Recommended Additions 62 | - use new **PersistGate** to delay rendering until rehydration is complete 63 | - `import { PersistGate } from 'redux-persist/lib/integration/react'` 64 | - set `config.debug = true` to get useful logging 65 | 66 | If your implementation uses getStoredState + createPersistor see [alternate migration](./v5-migration-alternate.md) 67 | 68 | ## Why v5 69 | Long story short, the changes are required in order to support new use cases 70 | - code splitting reducers 71 | - easier to ship persist support inside of other libs (e.g. redux-offline) 72 | - ability to colocate persistence rules with the reducer it pertains to 73 | - first class migration support 74 | - enable PersistGate react component which blocks rendering until persistence is complete (and enables similar patterns for integration) 75 | - possible to nest persistence 76 | - guarantee consistent state atoms 77 | - better debugability and extensibility 78 | 79 | ## Experimental v4 to v5 State Migration 80 | - **warning: this method is completely untested** 81 | - v5 getStoredState is not compatible with v4, so by default v5 will cause all of the persisted state from v4 to disappear on first run 82 | - v5 ships with an experimental v4 -> v5 migration that works by overriding the default getStoredState implementation 83 | **Warning** this is completely untested, please try and report back with any issues. 84 | ```js 85 | import getStoredStateMigrateV4 from 'redux-persist/lib/integration/getStoredStateMigrateV4' 86 | // ... 87 | persistReducer({ 88 | // ... 89 | getStoredState: getStoredStateMigrateV4(yourOldV4Config) 90 | }, baseReducer) 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/PersistGate.md: -------------------------------------------------------------------------------- 1 | `PersistGate` delays the rendering of your app's UI until your persisted state has been retrieved and saved to redux. 2 | 3 | **NOTE**: the `loading` prop can be `null` or any react instance to show during loading (e.g. a splash screen), for example `loading={}`. 4 | 5 | Example usage: 6 | 7 | ```js 8 | import { PersistGate } from 'redux-persist/es/integration/react' 9 | 10 | import configureStore from './store/configureStore' 11 | 12 | const { persistor, store } = configureStore() 13 | 14 | const onBeforeLift = () => { 15 | // take some action before the gate lifts 16 | } 17 | 18 | export default () => ( 19 | 20 | } 22 | onBeforeLift={onBeforeLift} 23 | persistor={persistor}> 24 | 25 | 26 | 27 | ) 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # Redux Persist API 2 | --- 3 | ## Standard API 4 | - [persistReducer](#persistreducerconfig-reducer)([config](#type-persistconfig), reducer) 5 | - [persistStore](#persiststorestore-config-callback)(store) 6 | - [createMigrate](#createmigratemigrations-config)([migrations](#type-migrationmanifest)) 7 | ### `persistReducer(config, reducer)` 8 | 9 | ```js 10 | persistReducer( 11 | config: PersistConfig, 12 | reducer: Reducer, 13 | ): Reducer 14 | ``` 15 | 16 | Where Reducer is any reducer `(state, action) => state` and PersistConfig is [defined below](#type-persistconfig) 17 | 18 | ### `persistStore(store, config, callback)` 19 | ```js 20 | persistStore( 21 | store: Store, 22 | config?: { enhancer?: Function }, 23 | callback?: () => {} 24 | ): Persistor 25 | ``` 26 | 27 | Where Persistor is [defined below](#type-persistor) 28 | 29 | ### `createMigrate(migrations, config)` 30 | ```js 31 | createMigrate( 32 | migrations: MigrationManifest, 33 | config?: { debug: boolean } 34 | ) 35 | ``` 36 | 37 | ### `type Persistor` 38 | ```js 39 | { 40 | purge: () => Promise, 41 | flush: () => Promise, 42 | } 43 | ``` 44 | 45 | The Persistor is a redux store unto itself, plus 46 | 1. the `purge()` method for clearing out stored state. 47 | 2. the `flush()` method for flushing all pending state serialization and immediately write to disk 48 | 49 | `purge()` method only clear the content of the storage, leaving the internal data of `redux` untouched. To clean it instead, you can use the [redux-reset](https://github.com/wwayne/redux-reset) module. 50 | 51 | ### `type PersistConfig` 52 | ```js 53 | { 54 | key: string, // the key for the persist 55 | storage: Object, // the storage adapter, following the AsyncStorage api 56 | version?: number, // the state version as an integer (defaults to -1) 57 | blacklist?: Array, // do not persist these keys 58 | whitelist?: Array, // only persist these keys 59 | migrate?: (Object, number) => Promise, 60 | transforms?: Array, 61 | throttle?: number, // ms to throttle state writes 62 | keyPrefix?: string, // will be prefixed to the storage key 63 | debug?: boolean, // true -> verbose logs 64 | stateReconciler?: false | StateReconciler, // false -> do not automatically reconcile state 65 | serialize?: boolean, // false -> do not call JSON.parse & stringify when setting & getting from storage 66 | writeFailHandler?: Function, // will be called if the storage engine fails during setItem() 67 | } 68 | ``` 69 | 70 | Persisting state involves calling setItem() on the storage engine. By default, this will fail silently if the storage/quota is exhausted. 71 | Provide a writeFailHandler(error) function to be notified if this occurs. 72 | 73 | ### `type MigrationManifest` 74 | ```js 75 | { 76 | [number]: (State) => State 77 | } 78 | ``` 79 | Where the keys are state version numbers and the values are migration functions to modify state. 80 | 81 | --- 82 | ## Expanded API 83 | The following methods are used internally by the standard api. They can be accessed directly if more control is needed. 84 | ### `getStoredState(config)` 85 | ```js 86 | getStoredState( 87 | config: PersistConfig 88 | ): Promise 89 | ``` 90 | 91 | Returns a promise (if Promise global is defined) of restored state. 92 | 93 | ### `createPersistoid(config)` 94 | ```js 95 | createPersistoid( 96 | config 97 | ): Persistoid 98 | ``` 99 | Where Persistoid is [defined below](#type-persistoid). 100 | 101 | ### `type Persistoid` 102 | ```js 103 | { 104 | update: (State) => void 105 | } 106 | ``` 107 | 108 | ### `type PersistorConfig` 109 | ```js 110 | { 111 | enhancer: Function 112 | } 113 | ``` 114 | Where enhancer will be sent verbatim to the redux createStore call used to create the persistor store. This can be useful for example to enable redux devtools on the persistor store. 115 | 116 | ### `type StateReconciler` 117 | ```js 118 | ( 119 | inboundState: State, 120 | originalState: State, 121 | reducedState: State, 122 | ) => State 123 | ``` 124 | A function which reconciles: 125 | - **inboundState**: the state being rehydrated from storage 126 | - **originalState**: the state before the REHYDRATE action 127 | - **reducedState**: the store state *after* the REHYDRATE action but *before* the reconcilliation 128 | into final "rehydrated" state. 129 | -------------------------------------------------------------------------------- /docs/hot-module-replacement.md: -------------------------------------------------------------------------------- 1 | ## Hot Module Replacement 2 | 3 | Hot Module Replacement (HMR) is a wonderful feature that is really useful in development environment. This allows you to update the code of your application without reloading the app and resetting the redux state. 4 | 5 | The key modification for using HMR with redux-persist, is the incoming hot reducer needs to be re-persisted via `persistReducer`. 6 | 7 | **configureStore.js** 8 | ```js 9 | import { persistReducer } from 'redux-persist' 10 | import rootReducer from './path/to/reducer' 11 | 12 | export default () => { 13 | // create store and persistor per normal... 14 | 15 | if (module.hot) { 16 | module.hot.accept('./path/to/reducer', () => { 17 | // This fetch the new state of the above reducers. 18 | const nextRootReducer = require('./path/to/reducer').default 19 | store.replaceReducer( 20 | persistReducer(persistConfig, nextRootReducer) 21 | ) 22 | }) 23 | } 24 | 25 | return { store, persistor } 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/migrations.md: -------------------------------------------------------------------------------- 1 | # Redux Persist Migration Example 2 | 3 | ### Example with createMigrate 4 | ```js 5 | import { createMigrate, persistReducer, persistStore } from 'redux-persist' 6 | import storage from 'redux-persist/es/storage' 7 | 8 | const migrations = { 9 | 0: (state) => { 10 | // migration clear out device state 11 | return { 12 | ...state, 13 | device: undefined 14 | } 15 | }, 16 | 1: (state) => { 17 | // migration to keep only device state 18 | return { 19 | device: state.device 20 | } 21 | } 22 | } 23 | 24 | const persistConfig = { 25 | key: 'primary', 26 | version: 1, 27 | storage, 28 | migrate: createMigrate(migrations, { debug: false }), 29 | } 30 | 31 | const finalReducer = persistReducer(persistConfig, reducer) 32 | 33 | export default function configureStore() { 34 | let store = createStore(finalReducer) 35 | let persistor = persistStore(store) 36 | return { store, persistor } 37 | } 38 | ``` 39 | 40 | ### Alternative 41 | The migrate method can be any function with which returns a promise of new state. 42 | ```js 43 | const persistConfig = { 44 | key: 'primary', 45 | version: 1, 46 | storage, 47 | migrate: (state) => { 48 | console.log('Migration Running!') 49 | return Promise.resolve(state) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/v5-migration-alternate.md: -------------------------------------------------------------------------------- 1 | ## Alternate Migration 2 | If in redux-persist you used getStoredState + createPersistor, the v5 usage is similar with some small modifications. Note: because no `persistor` is created the react integration helper `PersistGate` cannot be used. 3 | 4 | 1. replace `createPersistor` with `createPersistoid` 5 | 2. update persistoid whenever state changes 6 | 7 | ```js 8 | import { getStoredState } from 'redux-persist/es/getStoredState' 9 | import { createPersistoid } from 'redux-persist/es/createPersistoid' 10 | import storage from 'redux-persist/es/storages/local' 11 | 12 | // ... 13 | 14 | const config = { key: 'root', version: 1, storage } 15 | 16 | function configureStore () { 17 | const initState = await getStoredState(config) 18 | // createPersistoid instead of createPersistor 19 | let persistoid = createPersistoid(config) 20 | 21 | const store = createStore(reducer, initState) 22 | 23 | // need to hook up the subscription (this used to be done automatically by createPersistor) 24 | store.subscribe(() => { 25 | persistoid.update(store.getState()) 26 | }) 27 | } 28 | ``` -------------------------------------------------------------------------------- /integration/README.md: -------------------------------------------------------------------------------- 1 | Proxy package to enable 2 | ```js 3 | import { PersistGate } from 'redux-persist/integration/react' 4 | ``` -------------------------------------------------------------------------------- /integration/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-persist/integration/react", 3 | "private": true, 4 | "main": "../../lib/integration/react", 5 | "module": "../../es/integration/react", 6 | "jsnext:main": "../../es/integration/react" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-persist", 3 | "version": "6.1.0", 4 | "description": "persist and rehydrate redux stores", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "types": "lib/index.d.ts", 8 | "repository": "rt2zz/redux-persist", 9 | "files": [ 10 | "src", 11 | "es", 12 | "lib", 13 | "dist", 14 | "integration", 15 | "README.md" 16 | ], 17 | "scripts": { 18 | "ava": "ava", 19 | "build": "npm run build:commonjs && npm run build:es && npm run build:umd", 20 | "build:commonjs": "tsc --module commonjs --outDir lib", 21 | "build:es": "tsc --module es2015 --outDir es", 22 | "build:umd": "rollup -c", 23 | "clean": "rimraf dist && rimraf es && rimraf lib", 24 | "prepare": "npm run build", 25 | "precommit": "lint-staged", 26 | "stats:size": "node ./scripts/size-estimator.js", 27 | "test": "ava", 28 | "version": "npm run clean && npm run build && npm run stats:size | tail -1 >> LIBSIZE.md && git add LIBSIZE.md" 29 | }, 30 | "lint-staged": { 31 | "src/**/*.ts": [ 32 | "prettier --write", 33 | "git add" 34 | ] 35 | }, 36 | "author": "", 37 | "license": "MIT", 38 | "homepage": "https://github.com/rt2zz/redux-persist#readme", 39 | "ava": { 40 | "files": [ 41 | "tests/**/*.spec.ts" 42 | ], 43 | "extensions": [ 44 | "ts" 45 | ], 46 | "require": [ 47 | "ts-node/register" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.15.0", 52 | "@babel/preset-env": "^7.15.0", 53 | "@rollup/plugin-babel": "^5.3.0", 54 | "@rollup/plugin-commonjs": "^20.0.0", 55 | "@rollup/plugin-node-resolve": "^13.0.4", 56 | "@rollup/plugin-typescript": "^8.2.5", 57 | "@types/react": "^17.0.16", 58 | "@types/redux-mock-store": "^1.0.3", 59 | "@types/sinon": "^10.0.2", 60 | "@typescript-eslint/eslint-plugin": "^4.29.0", 61 | "@typescript-eslint/parser": "^4.29.0", 62 | "ava": "^3.15.0", 63 | "eslint": "^7.32.0", 64 | "eslint-plugin-import": "^2.23.4", 65 | "husky": "^7.0.1", 66 | "lint-staged": "^11.1.2", 67 | "prettier": "^2.3.2", 68 | "redux": "^4.1.1", 69 | "redux-mock-store": "^1.5.4", 70 | "rimraf": "^3.0.2", 71 | "rollup": "^2.56.0", 72 | "rollup-plugin-terser": "^7.0.2", 73 | "sinon": "^11.1.2", 74 | "ts-node": "^10.1.0", 75 | "typescript": "^4.3.5" 76 | }, 77 | "peerDependencies": { 78 | "redux": ">4.0.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pluginNodeResolve from "@rollup/plugin-node-resolve" 2 | import pluginCommonjs from "@rollup/plugin-commonjs" 3 | import pluginTypescript from "@rollup/plugin-typescript" 4 | import { babel as pluginBabel } from "@rollup/plugin-babel" 5 | import { terser as pluginTerser } from "rollup-plugin-terser" 6 | 7 | const moduleName = 'ReduxPersist' 8 | 9 | import * as path from 'path' 10 | 11 | import pkg from "./package.json" 12 | 13 | const banner = `/*! 14 | ${moduleName}.js v${pkg.version} 15 | ${pkg.homepage} 16 | Released under the ${pkg.license} License. 17 | */`; 18 | 19 | const filePath = 'dist/redux-persist.js' 20 | 21 | const config = [ 22 | // browser 23 | { 24 | // entry point 25 | input: 'src/index.ts', 26 | output: [ 27 | // no minify 28 | { 29 | name: moduleName, 30 | file: filePath, 31 | format: 'umd', 32 | sourcemap: true, 33 | // copyright 34 | banner, 35 | }, 36 | // minify 37 | { 38 | name: moduleName, 39 | file: filePath.replace('.js', '.min.js'), 40 | format: 'umd', 41 | sourcemap: true, 42 | banner, 43 | plugins: [ 44 | pluginTerser(), 45 | ], 46 | } 47 | ], 48 | plugins: [ 49 | pluginTypescript({ 50 | module: "esnext" 51 | }), 52 | pluginCommonjs({ 53 | extensions: [".js", ".ts"] 54 | }), 55 | pluginBabel({ 56 | babelHelpers: "bundled", 57 | configFile: path.resolve(__dirname, ".babelrc.js") 58 | }), 59 | pluginNodeResolve({ 60 | browser: true, 61 | }), 62 | ] 63 | }, 64 | ]; 65 | 66 | export default config 67 | 68 | -------------------------------------------------------------------------------- /scripts/size-estimator.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | const packageJson = require('../package.json') 3 | 4 | let packageVersion = packageJson.version 5 | // we estimate redux size based on the content length of the minified umd build hosted by unpkg. This script is brittle but works. 6 | let reduxSize = execSync("curl -sIL https://unpkg.com/redux/dist/redux.min.js | grep -i Content-Length | tail -1 | awk '{print $2}'").toString() 7 | // we need to substract redux size from our umd build to get an estimate of our first party code size 8 | let persistSize = execSync("wc -c < dist/redux-persist.min.js") - reduxSize 9 | 10 | // note: markdown formatted for conveinence when appending to LIBSIZE.md 11 | console.log(`**v${packageVersion}**: ${persistSize} Bytes `) 12 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const KEY_PREFIX = 'persist:' 2 | export const FLUSH = 'persist/FLUSH' 3 | export const REHYDRATE = 'persist/REHYDRATE' 4 | export const PAUSE = 'persist/PAUSE' 5 | export const PERSIST = 'persist/PERSIST' 6 | export const PURGE = 'persist/PURGE' 7 | export const REGISTER = 'persist/REGISTER' 8 | export const DEFAULT_VERSION = -1 9 | -------------------------------------------------------------------------------- /src/createMigrate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { DEFAULT_VERSION } from './constants' 3 | 4 | import type { PersistedState, MigrationManifest } from './types' 5 | 6 | export default function createMigrate( 7 | migrations: MigrationManifest, 8 | config?: { debug: boolean } 9 | ): (state: PersistedState, currentVersion: number) => Promise { 10 | const { debug } = config || {} 11 | return function( 12 | state: PersistedState, 13 | currentVersion: number 14 | ): Promise { 15 | if (!state) { 16 | if (process.env.NODE_ENV !== 'production' && debug) 17 | console.log('redux-persist: no inbound state, skipping migration') 18 | return Promise.resolve(undefined) 19 | } 20 | 21 | const inboundVersion: number = 22 | state._persist && state._persist.version !== undefined 23 | ? state._persist.version 24 | : DEFAULT_VERSION 25 | if (inboundVersion === currentVersion) { 26 | if (process.env.NODE_ENV !== 'production' && debug) 27 | console.log('redux-persist: versions match, noop migration') 28 | return Promise.resolve(state) 29 | } 30 | if (inboundVersion > currentVersion) { 31 | if (process.env.NODE_ENV !== 'production') 32 | console.error('redux-persist: downgrading version is not supported') 33 | return Promise.resolve(state) 34 | } 35 | 36 | const migrationKeys = Object.keys(migrations) 37 | .map(ver => parseInt(ver)) 38 | .filter(key => currentVersion >= key && key > inboundVersion) 39 | .sort((a, b) => a - b) 40 | 41 | if (process.env.NODE_ENV !== 'production' && debug) 42 | console.log('redux-persist: migrationKeys', migrationKeys) 43 | try { 44 | const migratedState: any = migrationKeys.reduce((state: any, versionKey) => { 45 | if (process.env.NODE_ENV !== 'production' && debug) 46 | console.log( 47 | 'redux-persist: running migration for versionKey', 48 | versionKey 49 | ) 50 | return migrations[versionKey](state) 51 | }, state) 52 | return Promise.resolve(migratedState) 53 | } catch (err) { 54 | return Promise.reject(err) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/createPersistoid.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { KEY_PREFIX } from './constants' 3 | 4 | import type { Persistoid, PersistConfig } from './types' 5 | import { KeyAccessState } from './types' 6 | 7 | export default function createPersistoid(config: PersistConfig): Persistoid { 8 | // defaults 9 | const blacklist: string[] | null = config.blacklist || null 10 | const whitelist: string[] | null = config.whitelist || null 11 | const transforms = config.transforms || [] 12 | const throttle = config.throttle || 0 13 | const storageKey = `${ 14 | config.keyPrefix !== undefined ? config.keyPrefix : KEY_PREFIX 15 | }${config.key}` 16 | const storage = config.storage 17 | let serialize: (x: any) => any 18 | if (config.serialize === false) { 19 | serialize = (x: any) => x 20 | } else if (typeof config.serialize === 'function') { 21 | serialize = config.serialize 22 | } else { 23 | serialize = defaultSerialize 24 | } 25 | const writeFailHandler = config.writeFailHandler || null 26 | 27 | // initialize stateful values 28 | let lastState: KeyAccessState = {} 29 | const stagedState: KeyAccessState = {} 30 | const keysToProcess: string[] = [] 31 | let timeIterator: any = null 32 | let writePromise: Promise | null = null 33 | 34 | const update = (state: KeyAccessState) => { 35 | // add any changed keys to the queue 36 | Object.keys(state).forEach(key => { 37 | if (!passWhitelistBlacklist(key)) return // is keyspace ignored? noop 38 | if (lastState[key] === state[key]) return // value unchanged? noop 39 | if (keysToProcess.indexOf(key) !== -1) return // is key already queued? noop 40 | keysToProcess.push(key) // add key to queue 41 | }) 42 | 43 | //if any key is missing in the new state which was present in the lastState, 44 | //add it for processing too 45 | Object.keys(lastState).forEach(key => { 46 | if ( 47 | state[key] === undefined && 48 | passWhitelistBlacklist(key) && 49 | keysToProcess.indexOf(key) === -1 && 50 | lastState[key] !== undefined 51 | ) { 52 | keysToProcess.push(key) 53 | } 54 | }) 55 | 56 | // start the time iterator if not running (read: throttle) 57 | if (timeIterator === null) { 58 | timeIterator = setInterval(processNextKey, throttle) 59 | } 60 | 61 | lastState = state 62 | } 63 | 64 | function processNextKey() { 65 | if (keysToProcess.length === 0) { 66 | if (timeIterator) clearInterval(timeIterator) 67 | timeIterator = null 68 | return 69 | } 70 | 71 | const key: any = keysToProcess.shift() 72 | if (key === undefined) { 73 | return 74 | } 75 | const endState = transforms.reduce((subState, transformer) => { 76 | return transformer.in(subState, key, lastState) 77 | }, lastState[key]) 78 | 79 | if (endState !== undefined) { 80 | try { 81 | stagedState[key] = serialize(endState) 82 | } catch (err) { 83 | console.error( 84 | 'redux-persist/createPersistoid: error serializing state', 85 | err 86 | ) 87 | } 88 | } else { 89 | //if the endState is undefined, no need to persist the existing serialized content 90 | delete stagedState[key] 91 | } 92 | 93 | if (keysToProcess.length === 0) { 94 | writeStagedState() 95 | } 96 | } 97 | 98 | function writeStagedState() { 99 | // cleanup any removed keys just before write. 100 | Object.keys(stagedState).forEach(key => { 101 | if (lastState[key] === undefined) { 102 | delete stagedState[key] 103 | } 104 | }) 105 | 106 | writePromise = storage 107 | .setItem(storageKey, serialize(stagedState)) 108 | .catch(onWriteFail) 109 | } 110 | 111 | function passWhitelistBlacklist(key: string) { 112 | if (whitelist && whitelist.indexOf(key) === -1 && key !== '_persist') 113 | return false 114 | if (blacklist && blacklist.indexOf(key) !== -1) return false 115 | return true 116 | } 117 | 118 | function onWriteFail(err: any) { 119 | // @TODO add fail handlers (typically storage full) 120 | if (writeFailHandler) writeFailHandler(err) 121 | if (err && process.env.NODE_ENV !== 'production') { 122 | console.error('Error storing data', err) 123 | } 124 | } 125 | 126 | const flush = () => { 127 | while (keysToProcess.length !== 0) { 128 | processNextKey() 129 | } 130 | return writePromise || Promise.resolve() 131 | } 132 | 133 | // return `persistoid` 134 | return { 135 | update, 136 | flush, 137 | } 138 | } 139 | 140 | // @NOTE in the future this may be exposed via config 141 | function defaultSerialize(data: any) { 142 | return JSON.stringify(data) 143 | } 144 | -------------------------------------------------------------------------------- /src/createTransform.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | type TransformConfig = { 3 | whitelist?: Array, 4 | blacklist?: Array, 5 | } 6 | 7 | export default function createTransform( 8 | // @NOTE inbound: transform state coming from redux on its way to being serialized and stored 9 | // eslint-disable-next-line @typescript-eslint/ban-types 10 | inbound: Function, 11 | // @NOTE outbound: transform state coming from storage, on its way to be rehydrated into redux 12 | // eslint-disable-next-line @typescript-eslint/ban-types 13 | outbound: Function, 14 | config: TransformConfig = {} 15 | ): any { 16 | const whitelist = config.whitelist || null 17 | const blacklist = config.blacklist || null 18 | 19 | function whitelistBlacklistCheck(key: string) { 20 | if (whitelist && whitelist.indexOf(key) === -1) return true 21 | if (blacklist && blacklist.indexOf(key) !== -1) return true 22 | return false 23 | } 24 | 25 | return { 26 | in: (state: Record, key: string, fullState: Record) => 27 | !whitelistBlacklistCheck(key) && inbound 28 | ? inbound(state, key, fullState) 29 | : state, 30 | out: (state: Record, key: string, fullState: Record) => 31 | !whitelistBlacklistCheck(key) && outbound 32 | ? outbound(state, key, fullState) 33 | : state, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/getStoredState.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { KeyAccessState, PersistConfig } from './types' 3 | 4 | import { KEY_PREFIX } from './constants' 5 | 6 | export default function getStoredState( 7 | config: PersistConfig 8 | ): Promise { 9 | const transforms = config.transforms || [] 10 | const storageKey = `${ 11 | config.keyPrefix !== undefined ? config.keyPrefix : KEY_PREFIX 12 | }${config.key}` 13 | const storage = config.storage 14 | const debug = config.debug 15 | let deserialize: (x: any) => any 16 | if (config.deserialize === false) { 17 | deserialize = (x: any) => x 18 | } else if (typeof config.deserialize === 'function') { 19 | deserialize = config.deserialize 20 | } else { 21 | deserialize = defaultDeserialize 22 | } 23 | return storage.getItem(storageKey).then((serialized: any) => { 24 | if (!serialized) return undefined 25 | else { 26 | try { 27 | const state: KeyAccessState = {} 28 | const rawState = deserialize(serialized) 29 | Object.keys(rawState).forEach(key => { 30 | state[key] = transforms.reduceRight((subState, transformer) => { 31 | return transformer.out(subState, key, rawState) 32 | }, deserialize(rawState[key])) 33 | }) 34 | return state 35 | } catch (err) { 36 | if (process.env.NODE_ENV !== 'production' && debug) 37 | console.log( 38 | `redux-persist/getStoredState: Error restoring data ${serialized}`, 39 | err 40 | ) 41 | throw err 42 | } 43 | } 44 | }) 45 | } 46 | 47 | function defaultDeserialize(serial: string) { 48 | return JSON.parse(serial) 49 | } 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as persistReducer } from './persistReducer' 2 | export { default as persistCombineReducers } from './persistCombineReducers' 3 | export { default as persistStore } from './persistStore' 4 | export { default as createMigrate } from './createMigrate' 5 | export { default as createTransform } from './createTransform' 6 | export { default as getStoredState } from './getStoredState' 7 | export { default as createPersistoid } from './createPersistoid' 8 | export { default as purgeStoredState } from './purgeStoredState' 9 | 10 | export * from './constants' 11 | -------------------------------------------------------------------------------- /src/integration/getStoredStateMigrateV4.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import getStoredStateV5 from '../getStoredState' 3 | 4 | import type { KeyAccessState, PersistConfig, Storage, Transform } from '../types' 5 | 6 | type V4Config = { 7 | storage?: Storage, 8 | serialize: boolean, 9 | keyPrefix?: string, 10 | transforms?: Array>, 11 | blacklist?: Array, 12 | whitelist?: Array, 13 | } 14 | 15 | export default function getStoredState(v4Config: V4Config) { 16 | return function(v5Config: PersistConfig): any { 17 | return getStoredStateV5(v5Config).then(state => { 18 | if (state) return state 19 | else return getStoredStateV4(v4Config) 20 | }) 21 | } 22 | } 23 | 24 | const KEY_PREFIX = 'reduxPersist:' 25 | 26 | function hasLocalStorage() { 27 | if (typeof self !== 'object' || !('localStorage' in self)) { 28 | return false 29 | } 30 | 31 | try { 32 | const storage = self.localStorage 33 | const testKey = `redux-persist localStorage test` 34 | storage.setItem(testKey, 'test') 35 | storage.getItem(testKey) 36 | storage.removeItem(testKey) 37 | } catch (e) { 38 | if (process.env.NODE_ENV !== 'production') 39 | console.warn( 40 | `redux-persist localStorage test failed, persistence will be disabled.` 41 | ) 42 | return false 43 | } 44 | return true 45 | } 46 | 47 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 48 | const noop = (...args: any) => { 49 | /* noop */ return null 50 | } 51 | const noStorage = { 52 | getItem: noop, 53 | setItem: noop, 54 | removeItem: noop, 55 | getAllKeys: noop, 56 | keys: [] 57 | } 58 | const createAsyncLocalStorage = () => { 59 | if (!hasLocalStorage()) return noStorage 60 | const localStorage = self.localStorage 61 | return { 62 | getAllKeys: function(cb: any) { 63 | try { 64 | const keys = [] 65 | for (let i = 0; i < localStorage.length; i++) { 66 | keys.push(localStorage.key(i)) 67 | } 68 | cb(null, keys) 69 | } catch (e) { 70 | cb(e) 71 | } 72 | }, 73 | getItem(key: string, cb: any) { 74 | try { 75 | const s = localStorage.getItem(key) 76 | cb(null, s) 77 | } catch (e) { 78 | cb(e) 79 | } 80 | }, 81 | setItem(key: string, string: string, cb: any) { 82 | try { 83 | localStorage.setItem(key, string) 84 | cb(null) 85 | } catch (e) { 86 | cb(e) 87 | } 88 | }, 89 | removeItem(key: string, cb: any) { 90 | try { 91 | localStorage.removeItem(key) 92 | cb && cb(null) 93 | } catch (e) { 94 | cb(e) 95 | } 96 | }, 97 | keys: localStorage.keys 98 | } 99 | } 100 | 101 | function getStoredStateV4(v4Config: V4Config) { 102 | return new Promise((resolve, reject) => { 103 | let storage = v4Config.storage || createAsyncLocalStorage() 104 | const deserializer = 105 | v4Config.serialize === false 106 | ? (data: any) => data 107 | : (serial: string) => JSON.parse(serial) 108 | const blacklist = v4Config.blacklist || [] 109 | const whitelist = v4Config.whitelist || false 110 | const transforms = v4Config.transforms || [] 111 | const keyPrefix = 112 | v4Config.keyPrefix !== undefined ? v4Config.keyPrefix : KEY_PREFIX 113 | 114 | // fallback getAllKeys to `keys` if present (LocalForage compatability) 115 | if (storage.keys && !storage.getAllKeys) 116 | storage = { ...storage, getAllKeys: storage.keys } 117 | 118 | const restoredState: KeyAccessState = {} 119 | let completionCount = 0 120 | 121 | storage.getAllKeys((err: any, allKeys:string[] = []) => { 122 | if (err) { 123 | if (process.env.NODE_ENV !== 'production') 124 | console.warn( 125 | 'redux-persist/getStoredState: Error in storage.getAllKeys' 126 | ) 127 | return reject(err) 128 | } 129 | 130 | const persistKeys = allKeys 131 | .filter(key => key.indexOf(keyPrefix) === 0) 132 | .map(key => key.slice(keyPrefix.length)) 133 | const keysToRestore = persistKeys.filter(passWhitelistBlacklist) 134 | 135 | const restoreCount = keysToRestore.length 136 | if (restoreCount === 0) resolve(undefined) 137 | keysToRestore.forEach(key => { 138 | storage.getItem(createStorageKey(key), (err: any, serialized: string) => { 139 | if (err && process.env.NODE_ENV !== 'production') 140 | console.warn( 141 | 'redux-persist/getStoredState: Error restoring data for key:', 142 | key, 143 | err 144 | ) 145 | else restoredState[key] = rehydrate(key, serialized) 146 | completionCount += 1 147 | if (completionCount === restoreCount) resolve(restoredState) 148 | }) 149 | }) 150 | }) 151 | 152 | function rehydrate(key: string, serialized: string) { 153 | let state = null 154 | 155 | try { 156 | const data = serialized ? deserializer(serialized) : undefined 157 | state = transforms.reduceRight((subState, transformer) => { 158 | return transformer.out(subState, key, {}) 159 | }, data) 160 | } catch (err) { 161 | if (process.env.NODE_ENV !== 'production') 162 | console.warn( 163 | 'redux-persist/getStoredState: Error restoring data for key:', 164 | key, 165 | err 166 | ) 167 | } 168 | 169 | return state 170 | } 171 | 172 | function passWhitelistBlacklist(key: string) { 173 | if (whitelist && whitelist.indexOf(key) === -1) return false 174 | if (blacklist.indexOf(key) !== -1) return false 175 | return true 176 | } 177 | 178 | function createStorageKey(key: string) { 179 | return `${keyPrefix}${key}` 180 | } 181 | }) 182 | } 183 | -------------------------------------------------------------------------------- /src/integration/react.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 2 | import React, { PureComponent, ReactNode } from 'react' 3 | import type { Persistor } from '../types' 4 | 5 | type Props = { 6 | onBeforeLift?: () => void, 7 | children: ReactNode | ((state: boolean) => ReactNode), 8 | loading: ReactNode, 9 | persistor: Persistor, 10 | } 11 | 12 | type State = { 13 | bootstrapped: boolean, 14 | } 15 | 16 | export class PersistGate extends PureComponent { 17 | static defaultProps = { 18 | children: null, 19 | loading: null, 20 | } 21 | 22 | state = { 23 | bootstrapped: false, 24 | } 25 | _unsubscribe?: () => void 26 | 27 | componentDidMount(): void { 28 | this._unsubscribe = this.props.persistor.subscribe( 29 | this.handlePersistorState 30 | ) 31 | this.handlePersistorState() 32 | } 33 | 34 | handlePersistorState = (): void => { 35 | const { persistor } = this.props 36 | const { bootstrapped } = persistor.getState() 37 | if (bootstrapped) { 38 | if (this.props.onBeforeLift) { 39 | Promise.resolve(this.props.onBeforeLift()) 40 | .finally(() => this.setState({ bootstrapped: true })) 41 | } else { 42 | this.setState({ bootstrapped: true }) 43 | } 44 | this._unsubscribe && this._unsubscribe() 45 | } 46 | } 47 | 48 | componentWillUnmount(): void { 49 | this._unsubscribe && this._unsubscribe() 50 | } 51 | 52 | render(): ReactNode { 53 | if (process.env.NODE_ENV !== 'production') { 54 | if (typeof this.props.children === 'function' && this.props.loading) 55 | console.error( 56 | 'redux-persist: PersistGate expects either a function child or loading prop, but not both. The loading prop will be ignored.' 57 | ) 58 | } 59 | if (typeof this.props.children === 'function') { 60 | return this.props.children(this.state.bootstrapped) 61 | } 62 | 63 | return this.state.bootstrapped ? this.props.children : this.props.loading 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/persistCombineReducers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Action, AnyAction, CombinedState, combineReducers, Reducer, ReducersMapObject } from 'redux' 3 | import persistReducer from './persistReducer' 4 | import autoMergeLevel2 from './stateReconciler/autoMergeLevel2' 5 | 6 | import type { 7 | PersistConfig 8 | } from './types' 9 | 10 | // combineReducers + persistReducer with stateReconciler defaulted to autoMergeLevel2 11 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 12 | export default function persistCombineReducers( 13 | config: PersistConfig, 14 | reducers: ReducersMapObject, Action> 15 | ): Reducer { 16 | config.stateReconciler = 17 | config.stateReconciler === undefined 18 | ? autoMergeLevel2 19 | : config.stateReconciler 20 | return persistReducer(config, combineReducers(reducers)) 21 | } 22 | -------------------------------------------------------------------------------- /src/persistReducer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | Action, AnyAction, Reducer 4 | } from 'redux' 5 | 6 | import { 7 | FLUSH, 8 | PAUSE, 9 | PERSIST, 10 | PURGE, 11 | REHYDRATE, 12 | DEFAULT_VERSION, 13 | } from './constants' 14 | 15 | import type { 16 | PersistConfig, 17 | PersistState, 18 | Persistoid, 19 | } from './types' 20 | 21 | import autoMergeLevel1 from './stateReconciler/autoMergeLevel1' 22 | import createPersistoid from './createPersistoid' 23 | import defaultGetStoredState from './getStoredState' 24 | import purgeStoredState from './purgeStoredState' 25 | 26 | type PersistPartial = { _persist: PersistState } | any; 27 | const DEFAULT_TIMEOUT = 5000 28 | /* 29 | @TODO add validation / handling for: 30 | - persisting a reducer which has nested _persist 31 | - handling actions that fire before reydrate is called 32 | */ 33 | export default function persistReducer( 34 | config: PersistConfig, 35 | baseReducer: Reducer 36 | ): Reducer { 37 | if (process.env.NODE_ENV !== 'production') { 38 | if (!config) throw new Error('config is required for persistReducer') 39 | if (!config.key) throw new Error('key is required in persistor config') 40 | if (!config.storage) 41 | throw new Error( 42 | "redux-persist: config.storage is required. Try using one of the provided storage engines `import storage from 'redux-persist/lib/storage'`" 43 | ) 44 | } 45 | 46 | const version = 47 | config.version !== undefined ? config.version : DEFAULT_VERSION 48 | const stateReconciler = 49 | config.stateReconciler === undefined 50 | ? autoMergeLevel1 51 | : config.stateReconciler 52 | const getStoredState = config.getStoredState || defaultGetStoredState 53 | const timeout = 54 | config.timeout !== undefined ? config.timeout : DEFAULT_TIMEOUT 55 | let _persistoid: Persistoid | null = null 56 | let _purge = false 57 | let _paused = true 58 | const conditionalUpdate = (state: any) => { 59 | // update the persistoid only if we are rehydrated and not paused 60 | state._persist.rehydrated && 61 | _persistoid && 62 | !_paused && 63 | _persistoid.update(state) 64 | return state 65 | } 66 | 67 | return (state: any, action: any) => { 68 | const { _persist, ...rest } = state || {} 69 | const restState: S = rest 70 | 71 | if (action.type === PERSIST) { 72 | let _sealed = false 73 | const _rehydrate = (payload: any, err?: Error) => { 74 | // dev warning if we are already sealed 75 | if (process.env.NODE_ENV !== 'production' && _sealed) 76 | console.error( 77 | `redux-persist: rehydrate for "${ 78 | config.key 79 | }" called after timeout.`, 80 | payload, 81 | err 82 | ) 83 | 84 | // only rehydrate if we are not already sealed 85 | if (!_sealed) { 86 | action.rehydrate(config.key, payload, err) 87 | _sealed = true 88 | } 89 | } 90 | timeout && 91 | setTimeout(() => { 92 | !_sealed && 93 | _rehydrate( 94 | undefined, 95 | new Error( 96 | `redux-persist: persist timed out for persist key "${ 97 | config.key 98 | }"` 99 | ) 100 | ) 101 | }, timeout) 102 | 103 | // @NOTE PERSIST resumes if paused. 104 | _paused = false 105 | 106 | // @NOTE only ever create persistoid once, ensure we call it at least once, even if _persist has already been set 107 | if (!_persistoid) _persistoid = createPersistoid(config) 108 | 109 | // @NOTE PERSIST can be called multiple times, noop after the first 110 | if (_persist) { 111 | // We still need to call the base reducer because there might be nested 112 | // uses of persistReducer which need to be aware of the PERSIST action 113 | return { 114 | ...baseReducer(restState, action), 115 | _persist, 116 | }; 117 | } 118 | 119 | if ( 120 | typeof action.rehydrate !== 'function' || 121 | typeof action.register !== 'function' 122 | ) 123 | throw new Error( 124 | '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.' 125 | ) 126 | 127 | action.register(config.key) 128 | 129 | getStoredState(config).then( 130 | restoredState => { 131 | if (restoredState) { 132 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 133 | const migrate = config.migrate || ((s, _) => Promise.resolve(s)) 134 | migrate(restoredState as any, version).then( 135 | migratedState => { 136 | _rehydrate(migratedState) 137 | }, 138 | migrateErr => { 139 | if (process.env.NODE_ENV !== 'production' && migrateErr) 140 | console.error('redux-persist: migration error', migrateErr) 141 | _rehydrate(undefined, migrateErr) 142 | } 143 | ) 144 | } 145 | }, 146 | err => { 147 | _rehydrate(undefined, err) 148 | } 149 | ) 150 | 151 | return { 152 | ...baseReducer(restState, action), 153 | _persist: { version, rehydrated: false }, 154 | } 155 | } else if (action.type === PURGE) { 156 | _purge = true 157 | action.result(purgeStoredState(config)) 158 | return { 159 | ...baseReducer(restState, action), 160 | _persist, 161 | } 162 | } else if (action.type === FLUSH) { 163 | action.result(_persistoid && _persistoid.flush()) 164 | return { 165 | ...baseReducer(restState, action), 166 | _persist, 167 | } 168 | } else if (action.type === PAUSE) { 169 | _paused = true 170 | } else if (action.type === REHYDRATE) { 171 | // noop on restState if purging 172 | if (_purge) 173 | return { 174 | ...restState, 175 | _persist: { ..._persist, rehydrated: true }, 176 | } 177 | 178 | // @NOTE if key does not match, will continue to default else below 179 | if (action.key === config.key) { 180 | const reducedState = baseReducer(restState, action) 181 | const inboundState = action.payload 182 | // only reconcile state if stateReconciler and inboundState are both defined 183 | const reconciledRest: S = 184 | stateReconciler !== false && inboundState !== undefined 185 | ? stateReconciler(inboundState, state, reducedState, config) 186 | : reducedState 187 | 188 | const newState = { 189 | ...reconciledRest, 190 | _persist: { ..._persist, rehydrated: true }, 191 | } 192 | return conditionalUpdate(newState) 193 | } 194 | } 195 | 196 | // if we have not already handled PERSIST, straight passthrough 197 | if (!_persist) return baseReducer(state, action) 198 | 199 | // run base reducer: 200 | // is state modified ? return original : return updated 201 | const newState = baseReducer(restState, action) 202 | if (newState === restState) return state 203 | return conditionalUpdate({ ...newState, _persist }) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/persistStore.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { 3 | Persistor, 4 | PersistorOptions, 5 | PersistorState, 6 | } from './types' 7 | 8 | import { AnyAction, createStore, Store } from 'redux' 9 | import { FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE } from './constants' 10 | 11 | type BoostrappedCb = () => any; 12 | 13 | const initialState: PersistorState = { 14 | registry: [], 15 | bootstrapped: false, 16 | } 17 | 18 | const persistorReducer = (state = initialState, action: AnyAction) => { 19 | const firstIndex = state.registry.indexOf(action.key) 20 | const registry = [...state.registry] 21 | switch (action.type) { 22 | case REGISTER: 23 | return { ...state, registry: [...state.registry, action.key] } 24 | case REHYDRATE: 25 | registry.splice(firstIndex, 1) 26 | return { ...state, registry, bootstrapped: registry.length === 0 } 27 | default: 28 | return state 29 | } 30 | } 31 | 32 | interface OptionToTestObject { 33 | [key: string]: any; 34 | } 35 | 36 | export default function persistStore( 37 | store: Store, 38 | options?: PersistorOptions, 39 | cb?: BoostrappedCb 40 | ): Persistor { 41 | // help catch incorrect usage of passing PersistConfig in as PersistorOptions 42 | if (process.env.NODE_ENV !== 'production') { 43 | const optionsToTest: OptionToTestObject = options || {} 44 | const bannedKeys = [ 45 | 'blacklist', 46 | 'whitelist', 47 | 'transforms', 48 | 'storage', 49 | 'keyPrefix', 50 | 'migrate', 51 | ] 52 | bannedKeys.forEach(k => { 53 | if (optionsToTest[k]) 54 | console.error( 55 | `redux-persist: invalid option passed to persistStore: "${k}". You may be incorrectly passing persistConfig into persistStore, whereas it should be passed into persistReducer.` 56 | ) 57 | }) 58 | } 59 | let boostrappedCb = cb || false 60 | 61 | const _pStore = createStore( 62 | persistorReducer, 63 | initialState, 64 | options && options.enhancer ? options.enhancer : undefined 65 | ) 66 | const register = (key: string) => { 67 | _pStore.dispatch({ 68 | type: REGISTER, 69 | key, 70 | }) 71 | } 72 | 73 | const rehydrate = (key: string, payload: Record, err: any) => { 74 | const rehydrateAction = { 75 | type: REHYDRATE, 76 | payload, 77 | err, 78 | key, 79 | } 80 | // dispatch to `store` to rehydrate and `persistor` to track result 81 | store.dispatch(rehydrateAction) 82 | _pStore.dispatch(rehydrateAction) 83 | if (typeof boostrappedCb === "function" && persistor.getState().bootstrapped) { 84 | boostrappedCb() 85 | boostrappedCb = false 86 | } 87 | } 88 | 89 | const persistor: Persistor = { 90 | ..._pStore, 91 | purge: () => { 92 | const results: Array = [] 93 | store.dispatch({ 94 | type: PURGE, 95 | result: (purgeResult: any) => { 96 | results.push(purgeResult) 97 | }, 98 | }) 99 | return Promise.all(results) 100 | }, 101 | flush: () => { 102 | const results: Array = [] 103 | store.dispatch({ 104 | type: FLUSH, 105 | result: (flushResult: any) => { 106 | results.push(flushResult) 107 | }, 108 | }) 109 | return Promise.all(results) 110 | }, 111 | pause: () => { 112 | store.dispatch({ 113 | type: PAUSE, 114 | }) 115 | }, 116 | persist: () => { 117 | store.dispatch({ type: PERSIST, register, rehydrate }) 118 | }, 119 | } 120 | 121 | if (!(options && options.manualPersist)){ 122 | persistor.persist() 123 | } 124 | 125 | return persistor 126 | } 127 | -------------------------------------------------------------------------------- /src/purgeStoredState.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { PersistConfig } from './types' 3 | 4 | import { KEY_PREFIX } from './constants' 5 | 6 | export default function purgeStoredState(config: PersistConfig):any { 7 | const storage = config.storage 8 | const storageKey = `${ 9 | config.keyPrefix !== undefined ? config.keyPrefix : KEY_PREFIX 10 | }${config.key}` 11 | return storage.removeItem(storageKey, warnIfRemoveError) 12 | } 13 | 14 | function warnIfRemoveError(err: any) { 15 | if (err && process.env.NODE_ENV !== 'production') { 16 | console.error( 17 | 'redux-persist/purgeStoredState: Error purging data stored state', 18 | err 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/stateReconciler/autoMergeLevel1.ts: -------------------------------------------------------------------------------- 1 | /* 2 | autoMergeLevel1: 3 | - merges 1 level of substate 4 | - skips substate if already modified 5 | */ 6 | 7 | import type { PersistConfig } from '../types' 8 | import { KeyAccessState } from '../types' 9 | 10 | export default function autoMergeLevel1( 11 | inboundState: S, 12 | originalState: S, 13 | reducedState: S, 14 | { debug }: PersistConfig 15 | ): S { 16 | const newState = { ...reducedState } 17 | // only rehydrate if inboundState exists and is an object 18 | if (inboundState && typeof inboundState === 'object') { 19 | const keys: (keyof S)[] = Object.keys(inboundState) 20 | keys.forEach(key => { 21 | // ignore _persist data 22 | if (key === '_persist') return 23 | // if reducer modifies substate, skip auto rehydration 24 | if (originalState[key] !== reducedState[key]) { 25 | if (process.env.NODE_ENV !== 'production' && debug) 26 | console.log( 27 | 'redux-persist/stateReconciler: sub state for key `%s` modified, skipping.', 28 | key 29 | ) 30 | return 31 | } 32 | // otherwise hard set the new value 33 | newState[key] = inboundState[key] 34 | }) 35 | } 36 | 37 | if ( 38 | process.env.NODE_ENV !== 'production' && 39 | debug && 40 | inboundState && 41 | typeof inboundState === 'object' 42 | ) 43 | console.log( 44 | `redux-persist/stateReconciler: rehydrated keys '${Object.keys( 45 | inboundState 46 | ).join(', ')}'` 47 | ) 48 | 49 | return newState 50 | } 51 | -------------------------------------------------------------------------------- /src/stateReconciler/autoMergeLevel2.ts: -------------------------------------------------------------------------------- 1 | /* 2 | autoMergeLevel2: 3 | - merges 2 level of substate 4 | - skips substate if already modified 5 | - this is essentially redux-perist v4 behavior 6 | */ 7 | 8 | import type { PersistConfig } from '../types' 9 | import { KeyAccessState } from '../types' 10 | 11 | export default function autoMergeLevel2( 12 | inboundState: S, 13 | originalState: S, 14 | reducedState: S, 15 | { debug }: PersistConfig 16 | ): S { 17 | const newState = { ...reducedState } 18 | // only rehydrate if inboundState exists and is an object 19 | if (inboundState && typeof inboundState === 'object') { 20 | const keys: (keyof S)[] = Object.keys(inboundState) 21 | keys.forEach(key => { 22 | // ignore _persist data 23 | if (key === '_persist') return 24 | // if reducer modifies substate, skip auto rehydration 25 | if (originalState[key] !== reducedState[key]) { 26 | if (process.env.NODE_ENV !== 'production' && debug) 27 | console.log( 28 | 'redux-persist/stateReconciler: sub state for key `%s` modified, skipping.', 29 | key 30 | ) 31 | return 32 | } 33 | if (isPlainEnoughObject(reducedState[key])) { 34 | // if object is plain enough shallow merge the new values (hence "Level2") 35 | newState[key] = { ...newState[key], ...inboundState[key] } 36 | return 37 | } 38 | // otherwise hard set 39 | newState[key] = inboundState[key] 40 | }) 41 | } 42 | 43 | if ( 44 | process.env.NODE_ENV !== 'production' && 45 | debug && 46 | inboundState && 47 | typeof inboundState === 'object' 48 | ) 49 | console.log( 50 | `redux-persist/stateReconciler: rehydrated keys '${Object.keys( 51 | inboundState 52 | ).join(', ')}'` 53 | ) 54 | 55 | return newState 56 | } 57 | 58 | function isPlainEnoughObject(o: unknown) { 59 | return o !== null && !Array.isArray(o) && typeof o === 'object' 60 | } 61 | -------------------------------------------------------------------------------- /src/stateReconciler/hardSet.ts: -------------------------------------------------------------------------------- 1 | /* 2 | hardSet: 3 | - hard set incoming state 4 | */ 5 | 6 | export default function hardSet(inboundState: S): S { 7 | return inboundState 8 | } 9 | -------------------------------------------------------------------------------- /src/storage/createWebStorage.ts: -------------------------------------------------------------------------------- 1 | import getStorage from './getStorage' 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export default function createWebStorage(type: string): any { 5 | const storage = getStorage(type) 6 | return { 7 | getItem: (key: string): Promise => { 8 | return new Promise((resolve) => { 9 | resolve(storage.getItem(key)) 10 | }) 11 | }, 12 | setItem: (key: string, item: string): Promise => { 13 | return new Promise((resolve) => { 14 | resolve(storage.setItem(key, item)) 15 | }) 16 | }, 17 | removeItem: (key: string): Promise => { 18 | return new Promise((resolve) => { 19 | resolve(storage.removeItem(key)) 20 | }) 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/storage/getStorage.ts: -------------------------------------------------------------------------------- 1 | import type { Storage } from '../types' 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-empty-function 4 | function noop() {} 5 | const noopStorage = { 6 | getItem: noop, 7 | setItem: noop, 8 | removeItem: noop, 9 | keys: [], 10 | getAllKeys: noop, 11 | } 12 | 13 | function hasStorage(storageType: string) { 14 | if (typeof self !== 'object' || !(storageType in self)) { 15 | return false 16 | } 17 | 18 | try { 19 | const storage = (self as unknown as { [key: string]: Storage})[storageType] as unknown as Storage 20 | const testKey = `redux-persist ${storageType} test` 21 | storage.setItem(testKey, 'test') 22 | storage.getItem(testKey) 23 | storage.removeItem(testKey) 24 | } catch (e) { 25 | if (process.env.NODE_ENV !== 'production') 26 | console.warn( 27 | `redux-persist ${storageType} test failed, persistence will be disabled.` 28 | ) 29 | return false 30 | } 31 | return true 32 | } 33 | 34 | export default function getStorage(type: string): Storage { 35 | const storageType = `${type}Storage` 36 | if (hasStorage(storageType)) return (self as unknown as { [key: string]: Storage })[storageType] 37 | else { 38 | if (process.env.NODE_ENV !== 'production') { 39 | console.error( 40 | `redux-persist failed to create sync storage. falling back to noop storage.` 41 | ) 42 | } 43 | return noopStorage 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/storage/index.ts: -------------------------------------------------------------------------------- 1 | import createWebStorage from './createWebStorage' 2 | 3 | export default createWebStorage('local') 4 | -------------------------------------------------------------------------------- /src/storage/session.ts: -------------------------------------------------------------------------------- 1 | import createWebStorage from './createWebStorage' 2 | 3 | export default createWebStorage('session') 4 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { REHYDRATE, REGISTER } from './constants' 4 | 5 | import { StoreEnhancer } from "redux"; 6 | 7 | export interface PersistState { 8 | version: number; 9 | rehydrated: boolean; 10 | } 11 | 12 | export type PersistedState = { 13 | _persist: PersistState; 14 | } | undefined; 15 | 16 | export type PersistMigrate = 17 | (state: PersistedState, currentVersion: number) => Promise; 18 | 19 | export type StateReconciler = 20 | (inboundState: any, state: S, reducedState: S, config: PersistConfig) => S; 21 | 22 | export interface KeyAccessState { 23 | [key: string]: any; 24 | } 25 | 26 | /** 27 | * @desc 28 | * `HSS` means HydratedSubState 29 | * `ESS` means EndSubState 30 | * `S` means State 31 | * `RS` means RawState 32 | */ 33 | export interface PersistConfig { 34 | version?: number; 35 | storage: Storage; 36 | key: string; 37 | /** 38 | * @deprecated keyPrefix is going to be removed in v6. 39 | */ 40 | keyPrefix?: string; 41 | blacklist?: Array; 42 | whitelist?: Array; 43 | transforms?: Array>; 44 | throttle?: number; 45 | migrate?: PersistMigrate; 46 | stateReconciler?: false | StateReconciler; 47 | /** 48 | * @desc Used for migrations. 49 | */ 50 | getStoredState?: (config: PersistConfig) => Promise; 51 | debug?: boolean; 52 | serialize?: boolean; 53 | deserialize?: boolean | ((x: any) => any); 54 | timeout?: number; 55 | writeFailHandler?: (err: Error) => void; 56 | } 57 | 58 | export interface PersistorOptions { 59 | enhancer?: StoreEnhancer; 60 | manualPersist?: boolean; 61 | } 62 | 63 | export interface Storage { 64 | getItem(key: string, ...args: Array): any; 65 | setItem(key: string, value: any, ...args: Array): any; 66 | removeItem(key: string, ...args: Array): any; 67 | keys?: Array; 68 | getAllKeys(cb?: any): any; 69 | } 70 | 71 | export interface WebStorage extends Storage { 72 | /** 73 | * @desc Fetches key and returns item in a promise. 74 | */ 75 | getItem(key: string): Promise; 76 | /** 77 | * @desc Sets value for key and returns item in a promise. 78 | */ 79 | setItem(key: string, item: string): Promise; 80 | /** 81 | * @desc Removes value for key. 82 | */ 83 | removeItem(key: string): Promise; 84 | } 85 | 86 | export interface MigrationManifest { 87 | [key: string]: (state: PersistedState) => PersistedState; 88 | } 89 | 90 | /** 91 | * @desc 92 | * `SS` means SubState 93 | * `ESS` means EndSubState 94 | * `S` means State 95 | */ 96 | export type TransformInbound = 97 | (subState: SS, key: keyof S, state: S) => ESS; 98 | 99 | /** 100 | * @desc 101 | * `SS` means SubState 102 | * `HSS` means HydratedSubState 103 | * `RS` means RawState 104 | */ 105 | export type TransformOutbound = 106 | (state: SS, key: keyof RS, rawState: RS) => HSS; 107 | 108 | export interface Transform { 109 | in: TransformInbound; 110 | out: TransformOutbound; 111 | } 112 | 113 | export type RehydrateErrorType = any; 114 | 115 | export interface RehydrateAction { 116 | type: typeof REHYDRATE; 117 | key: string; 118 | payload?: object | null; 119 | err?: RehydrateErrorType | null; 120 | } 121 | 122 | export interface Persistoid { 123 | update(state: object): void; 124 | flush(): Promise; 125 | } 126 | 127 | export interface RegisterAction { 128 | type: typeof REGISTER; 129 | key: string; 130 | } 131 | 132 | export type PersistorAction = 133 | | RehydrateAction 134 | | RegisterAction 135 | ; 136 | 137 | export interface PersistorState { 138 | registry: Array; 139 | bootstrapped: boolean; 140 | } 141 | 142 | export type PersistorSubscribeCallback = () => any; 143 | 144 | /** 145 | * A persistor is a redux store unto itself, allowing you to purge stored state, flush all 146 | * pending state serialization and immediately write to disk 147 | */ 148 | export interface Persistor { 149 | pause(): void; 150 | persist(): void; 151 | purge(): Promise; 152 | flush(): Promise; 153 | dispatch(action: PersistorAction): PersistorAction; 154 | getState(): PersistorState; 155 | subscribe(callback: PersistorSubscribeCallback): () => any; 156 | } 157 | -------------------------------------------------------------------------------- /tests/complete.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { combineReducers, createStore } from 'redux' 3 | 4 | import persistReducer from '../src/persistReducer' 5 | import persistStore from '../src/persistStore' 6 | import createMemoryStorage from './utils/createMemoryStorage' 7 | import brokenStorage from './utils/brokenStorage' 8 | 9 | const reducer = () => ({}) 10 | const config = { 11 | key: 'persist-reducer-test', 12 | version: 1, 13 | storage: createMemoryStorage(), 14 | debug: true, 15 | timeout: 5, 16 | } 17 | 18 | test('multiple persistReducers work together', t => { 19 | return new Promise((resolve) => { 20 | const r1 = persistReducer(config, reducer) 21 | const r2 = persistReducer(config, reducer) 22 | const rootReducer = combineReducers({ r1, r2 }) 23 | const store = createStore(rootReducer) 24 | const persistor = persistStore(store, {}, () => { 25 | t.is(persistor.getState().bootstrapped, true) 26 | resolve() 27 | }) 28 | }) 29 | }) 30 | 31 | test('persistStore timeout 0 never bootstraps', t => { 32 | return new Promise((resolve, reject) => { 33 | const r1 = persistReducer({...config, storage: brokenStorage, timeout: 0}, reducer) 34 | const rootReducer = combineReducers({ r1 }) 35 | const store = createStore(rootReducer) 36 | const persistor = persistStore(store, undefined, () => { 37 | console.log('resolve') 38 | reject() 39 | }) 40 | setTimeout(() => { 41 | t.is(persistor.getState().bootstrapped, false) 42 | resolve() 43 | }, 10) 44 | }) 45 | }) 46 | 47 | 48 | test('persistStore timeout forces bootstrap', t => { 49 | return new Promise((resolve, reject) => { 50 | const r1 = persistReducer({...config, storage: brokenStorage}, reducer) 51 | const rootReducer = combineReducers({ r1 }) 52 | const store = createStore(rootReducer) 53 | const persistor = persistStore(store, undefined, () => { 54 | t.is(persistor.getState().bootstrapped, true) 55 | resolve() 56 | }) 57 | setTimeout(() => { 58 | reject() 59 | }, 10) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /tests/createPersistor.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import sinon from 'sinon' 3 | import createMemoryStorage from './utils/createMemoryStorage' 4 | import createPersistoid from '../src/createPersistoid' 5 | const memoryStorage = createMemoryStorage() 6 | 7 | const config = { 8 | key: 'persist-reducer-test', 9 | version: 1, 10 | storage: memoryStorage, 11 | debug: true 12 | } 13 | 14 | let spy: sinon.SinonSpy; 15 | let clock: sinon.SinonFakeTimers; 16 | 17 | test.beforeEach(() => { 18 | spy = sinon.spy(memoryStorage, 'setItem') 19 | clock = sinon.useFakeTimers() 20 | }); 21 | 22 | test.afterEach(() => { 23 | spy.restore() 24 | clock.restore() 25 | }); 26 | 27 | // @NOTE these tests broke when updating sinon 28 | test.skip('it updates changed state', t => { 29 | const { update } = createPersistoid(config) 30 | update({ a: 1 }) 31 | clock.tick(1); 32 | update({ a: 2 }) 33 | clock.tick(1); 34 | t.true(spy.calledTwice); 35 | t.true(spy.withArgs('persist:persist-reducer-test', '{"a":"1"}').calledOnce); 36 | t.true(spy.withArgs('persist:persist-reducer-test', '{"a":"2"}').calledOnce); 37 | }) 38 | 39 | test.skip('it does not update unchanged state', t => { 40 | const { update } = createPersistoid(config) 41 | update({ a: undefined, b: 1 }) 42 | clock.tick(1); 43 | // This update should not cause a write. 44 | update({ a: undefined, b: 1 }) 45 | clock.tick(1); 46 | t.true(spy.calledOnce); 47 | t.true(spy.withArgs('persist:persist-reducer-test', '{"b":"1"}').calledOnce); 48 | }) 49 | 50 | test.skip('it updates removed keys', t => { 51 | const { update } = createPersistoid(config) 52 | update({ a: undefined, b: 1 }) 53 | clock.tick(1); 54 | update({ a: undefined, b: undefined }) 55 | clock.tick(1); 56 | t.true(spy.calledTwice); 57 | t.true(spy.withArgs('persist:persist-reducer-test', '{"b":"1"}').calledOnce); 58 | t.true(spy.withArgs('persist:persist-reducer-test', '{}').calledOnce); 59 | }) 60 | -------------------------------------------------------------------------------- /tests/flush.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import test from 'ava' 3 | import { createStore } from 'redux' 4 | 5 | import getStoredState from '../src/getStoredState' 6 | import persistReducer from '../src/persistReducer' 7 | import persistStore from '../src/persistStore' 8 | import createMemoryStorage from './utils/createMemoryStorage' 9 | 10 | const INCREMENT = 'INCREMENT' 11 | 12 | interface StateObject { 13 | [key: string]: any; 14 | } 15 | const initialState: StateObject = { a: 0, b: 10, c: 100} 16 | const reducer = (state = initialState, { type }: { type: any }) => { 17 | console.log('action', type) 18 | if (type === INCREMENT) { 19 | const result = state 20 | Object.keys(state).forEach((key) => { 21 | result[key] = state[key] + 1 22 | }) 23 | return result 24 | } 25 | return state 26 | } 27 | 28 | const memoryStorage = createMemoryStorage() 29 | 30 | const config = { 31 | key: 'persist-reducer-test', 32 | version: 1, 33 | storage: memoryStorage, 34 | debug: true, 35 | throttle: 1000, 36 | } 37 | 38 | test('state before flush is not updated, after flush is', t => { 39 | return new Promise((resolve) => { 40 | const rootReducer = persistReducer(config, reducer) 41 | const store = createStore(rootReducer) 42 | const persistor = persistStore(store, {}, async () => { 43 | store.dispatch({ type: INCREMENT }) 44 | const state = store.getState() 45 | const storedPreFlush = await getStoredState(config) 46 | t.not(storedPreFlush && storedPreFlush.c, state.c) 47 | await persistor.flush() 48 | const storedPostFlush = await getStoredState(config) 49 | resolve(t.is(storedPostFlush && storedPostFlush.c, state.c)) 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /tests/persistCombineReducers.spec.ts: -------------------------------------------------------------------------------- 1 | import persistCombineReducers from '../src/persistCombineReducers' 2 | import createMemoryStorage from './utils/createMemoryStorage' 3 | 4 | import test from 'ava' 5 | 6 | const config = { 7 | key: 'TestConfig', 8 | storage: createMemoryStorage() 9 | } 10 | 11 | test('persistCombineReducers returns a function', t => { 12 | const reducer = persistCombineReducers(config, { 13 | foo: () => ({}) 14 | }) 15 | 16 | t.is(typeof reducer, 'function') 17 | }) 18 | 19 | /* 20 | test.skip('persistCombineReducers merges two levels deep of state', t => { 21 | 22 | }) 23 | */ 24 | -------------------------------------------------------------------------------- /tests/persistReducer.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import sinon from 'sinon' 3 | 4 | import persistReducer from '../src/persistReducer' 5 | import createMemoryStorage from './utils/createMemoryStorage' 6 | import { PERSIST } from '../src/constants' 7 | import sleep from './utils/sleep' 8 | 9 | const reducer = () => ({}) 10 | const config = { 11 | key: 'persist-reducer-test', 12 | version: 1, 13 | storage: createMemoryStorage() 14 | } 15 | 16 | test('persistedReducer does not automatically set _persist state', t => { 17 | const persistedReducer = persistReducer(config, reducer) 18 | const state = persistedReducer({}, {type: "UNDEFINED"}) 19 | console.log('state', state) 20 | t.is(undefined, state._persist) 21 | }) 22 | 23 | test('persistedReducer does returns versioned, rehydrate tracked _persist state upon PERSIST', t => { 24 | const persistedReducer = persistReducer(config, reducer) 25 | const register = sinon.spy() 26 | const rehydrate = sinon.spy() 27 | const state = persistedReducer({}, { type: PERSIST, register, rehydrate }) 28 | t.deepEqual({ version: 1, rehydrated: false}, state._persist) 29 | }) 30 | 31 | test('persistedReducer calls register and rehydrate after PERSIST', async (t) => { 32 | const persistedReducer = persistReducer(config, reducer) 33 | const register = sinon.spy() 34 | const rehydrate = sinon.spy() 35 | persistedReducer({}, { type: PERSIST, register, rehydrate }) 36 | await sleep(5000) 37 | t.is(register.callCount, 1) 38 | t.is(rehydrate.callCount, 1) 39 | }) 40 | -------------------------------------------------------------------------------- /tests/persistStore.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import sinon from 'sinon' 3 | 4 | import configureStore from 'redux-mock-store' 5 | 6 | import persistStore from '../src/persistStore' 7 | import { PERSIST, REHYDRATE } from '../src/constants' 8 | import find from './utils/find' 9 | 10 | const mockStore = configureStore([]) 11 | 12 | test('persistStore dispatches PERSIST action', t => { 13 | const store = mockStore() 14 | persistStore(store) 15 | const actions = store.getActions() 16 | const persistAction = find(actions, { type: PERSIST }) 17 | t.truthy(persistAction) 18 | }) 19 | 20 | test('register method adds a key to the registry', t => { 21 | const store = mockStore() 22 | const persistor = persistStore(store) 23 | const actions = store.getActions() 24 | const persistAction = find(actions, { type: PERSIST }) 25 | persistAction.register('canary') 26 | t.deepEqual(persistor.getState().registry, ['canary']) 27 | }) 28 | 29 | test('rehydrate method fires with the expected shape', t => { 30 | const store = mockStore() 31 | persistStore(store) 32 | const actions = store.getActions() 33 | const persistAction = find(actions, { type: PERSIST }) 34 | persistAction.rehydrate('canary', { foo: 'bar' }, null) 35 | const rehydrateAction = find(actions, { type: REHYDRATE }) 36 | t.deepEqual(rehydrateAction, { type: REHYDRATE, key: 'canary', payload: { foo: 'bar' }, err: null }) 37 | }) 38 | 39 | test('rehydrate method removes provided key from registry', t => { 40 | const store = mockStore() 41 | const persistor = persistStore(store) 42 | const actions = store.getActions() 43 | const persistAction = find(actions, { type: PERSIST }) 44 | 45 | // register canary 46 | persistAction.register('canary') 47 | t.deepEqual(persistor.getState().registry, ['canary']) 48 | 49 | // rehydrate canary 50 | persistAction.rehydrate('canary', { foo: 'bar' }, null) 51 | t.deepEqual(persistor.getState().registry, []) 52 | }) 53 | 54 | test('rehydrate method removes exactly one of provided key from registry', t => { 55 | const store = mockStore() 56 | const persistor = persistStore(store) 57 | const actions = store.getActions() 58 | const persistAction = find(actions, { type: PERSIST }) 59 | 60 | // register canary twice 61 | persistAction.register('canary') 62 | persistAction.register('canary') 63 | t.deepEqual(persistor.getState().registry, ['canary', 'canary']) 64 | 65 | // rehydrate canary 66 | persistAction.rehydrate('canary', { foo: 'bar' }, null) 67 | t.deepEqual(persistor.getState().registry, ['canary']) 68 | }) 69 | 70 | test('once registry is cleared for first time, persistor is flagged as bootstrapped', t => { 71 | const store = mockStore() 72 | const persistor = persistStore(store) 73 | const actions = store.getActions() 74 | const persistAction = find(actions, { type: PERSIST }) 75 | 76 | persistAction.register('canary') 77 | t.false(persistor.getState().bootstrapped) 78 | persistAction.rehydrate('canary', { foo: 'bar' }, null) 79 | t.true(persistor.getState().bootstrapped) 80 | }) 81 | 82 | test('once persistor is flagged as bootstrapped, further registry changes do not affect this value', t => { 83 | const store = mockStore() 84 | const persistor = persistStore(store) 85 | const actions = store.getActions() 86 | const persistAction = find(actions, { type: PERSIST }) 87 | 88 | persistAction.register('canary') 89 | t.false(persistor.getState().bootstrapped) 90 | persistAction.rehydrate('canary', { foo: 'bar' }, null) 91 | t.true(persistor.getState().bootstrapped) 92 | 93 | // add canary back, registry is updated but bootstrapped remains true 94 | persistAction.register('canary') 95 | t.deepEqual(persistor.getState().registry, ['canary']) 96 | t.true(persistor.getState().bootstrapped) 97 | }) 98 | 99 | test('persistStore calls bootstrapped callback (at most once) if provided', t => { 100 | const store = mockStore() 101 | const bootstrappedCb = sinon.spy() 102 | persistStore(store, {}, bootstrappedCb) 103 | const actions = store.getActions() 104 | const persistAction = find(actions, { type: PERSIST }) 105 | 106 | persistAction.register('canary') 107 | persistAction.rehydrate('canary', { foo: 'bar' }, null) 108 | t.is(bootstrappedCb.callCount, 1) 109 | 110 | // further rehydrates do not trigger the cb 111 | persistAction.register('canary') 112 | persistAction.rehydrate('canary', { foo: 'bar' }, null) 113 | t.is(bootstrappedCb.callCount, 1) 114 | }) 115 | -------------------------------------------------------------------------------- /tests/utils/brokenStorage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | /* eslint-disable @typescript-eslint/ban-types */ 4 | export default { 5 | getItem(): Promise { 6 | return new Promise((resolve: Function, reject: Function) => {}) 7 | }, 8 | setItem(): Promise { 9 | return new Promise((resolve: Function, reject: Function) => {}) 10 | }, 11 | removeItem(): Promise { 12 | return new Promise((resolve: Function, reject: Function) => {}) 13 | }, 14 | getAllKeys(): Promise { 15 | return new Promise((resolve: Function, reject: Function) => {}) 16 | }, 17 | keys: [] 18 | } 19 | -------------------------------------------------------------------------------- /tests/utils/createMemoryStorage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Storage } from "../../src/types" 3 | 4 | interface StateObj { 5 | [key: string]: any; 6 | } 7 | 8 | export function createMemoryStorage():Storage { 9 | const state: StateObj = {} 10 | return { 11 | getItem(key: string): Promise { 12 | return Promise.resolve(state[key]) 13 | }, 14 | setItem(key: string, value: any): Promise { 15 | state[key] = value 16 | return Promise.resolve(value) 17 | }, 18 | removeItem(key: string): Promise { 19 | delete state[key] 20 | return Promise.resolve() 21 | }, 22 | getAllKeys(): Promise> { 23 | return Promise.resolve(Object.keys(state)) 24 | }, 25 | keys: Object.keys(state) 26 | } 27 | } 28 | 29 | export default createMemoryStorage 30 | -------------------------------------------------------------------------------- /tests/utils/find.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export default (collection: Array>, predicate: Record): any => { 3 | let result = {} 4 | collection.forEach((value: any) => { 5 | if (value.type && value.type === predicate.type) { 6 | result = value 7 | } 8 | }) 9 | return result 10 | } 11 | -------------------------------------------------------------------------------- /tests/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export default function (timeout: number): Promise { 2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 3 | return new Promise((resolve, _) => { 4 | setTimeout(resolve, timeout) 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | }, 72 | "include": [ 73 | "src/**/*" 74 | ], 75 | "exclude": [ 76 | "node_modules" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /types/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "redux-persist/es/types" { 2 | import { StoreEnhancer } from "redux"; 3 | 4 | interface PersistState { 5 | version: number; 6 | rehydrated: boolean; 7 | } 8 | 9 | type PersistedState = { 10 | _persist: PersistState; 11 | } | undefined; 12 | 13 | type PersistMigrate = 14 | (state: PersistedState, currentVersion: number) => Promise; 15 | 16 | type StateReconciler = 17 | (inboundState: any, state: S, reducedState: S, config: PersistConfig) => S; 18 | 19 | /** 20 | * @desc 21 | * `HSS` means HydratedSubState 22 | * `ESS` means EndSubState 23 | * `S` means State 24 | * `RS` means RawState 25 | */ 26 | interface PersistConfig { 27 | version?: number; 28 | storage: Storage; 29 | key: string; 30 | /** 31 | * @deprecated keyPrefix is going to be removed in v6. 32 | */ 33 | keyPrefix?: string; 34 | blacklist?: Array; 35 | whitelist?: Array; 36 | transforms?: Array>; 37 | throttle?: number; 38 | migrate?: PersistMigrate; 39 | stateReconciler?: false | StateReconciler; 40 | /** 41 | * @desc Used for migrations. 42 | */ 43 | getStoredState?: (config: PersistConfig) => Promise; 44 | debug?: boolean; 45 | serialize?: boolean; 46 | timeout?: number; 47 | writeFailHandler?: (err: Error) => void; 48 | } 49 | 50 | interface PersistorOptions { 51 | enhancer?: StoreEnhancer; 52 | manualPersist?: boolean; 53 | } 54 | 55 | interface Storage { 56 | getItem(key: string, ...args: Array): any; 57 | setItem(key: string, value: any, ...args: Array): any; 58 | removeItem(key: string, ...args: Array): any; 59 | } 60 | 61 | interface WebStorage extends Storage { 62 | /** 63 | * @desc Fetches key and returns item in a promise. 64 | */ 65 | getItem(key: string): Promise; 66 | /** 67 | * @desc Sets value for key and returns item in a promise. 68 | */ 69 | setItem(key: string, item: string): Promise; 70 | /** 71 | * @desc Removes value for key. 72 | */ 73 | removeItem(key: string): Promise; 74 | } 75 | 76 | interface MigrationManifest { 77 | [key: string]: (state: PersistedState) => PersistedState; 78 | } 79 | 80 | /** 81 | * @desc 82 | * `SS` means SubState 83 | * `ESS` means EndSubState 84 | * `S` means State 85 | */ 86 | type TransformInbound = 87 | (subState: SS, key: keyof S, state: S) => ESS; 88 | 89 | /** 90 | * @desc 91 | * `SS` means SubState 92 | * `HSS` means HydratedSubState 93 | * `RS` means RawState 94 | */ 95 | type TransformOutbound = 96 | (state: SS, key: keyof RS, rawState: RS) => HSS; 97 | 98 | interface Transform { 99 | in: TransformInbound; 100 | out: TransformOutbound; 101 | } 102 | 103 | type RehydrateErrorType = any; 104 | 105 | interface RehydrateAction { 106 | type: 'persist/REHYDRATE'; 107 | key: string; 108 | payload?: object | null; 109 | err?: RehydrateErrorType | null; 110 | } 111 | 112 | interface Persistoid { 113 | update(state: object): void; 114 | flush(): Promise; 115 | } 116 | 117 | interface RegisterAction { 118 | type: 'persist/REGISTER'; 119 | key: string; 120 | } 121 | 122 | type PersistorAction = 123 | | RehydrateAction 124 | | RegisterAction 125 | ; 126 | 127 | interface PersistorState { 128 | registry: Array; 129 | bootstrapped: boolean; 130 | } 131 | 132 | type PersistorSubscribeCallback = () => any; 133 | 134 | /** 135 | * A persistor is a redux store unto itself, allowing you to purge stored state, flush all 136 | * pending state serialization and immediately write to disk 137 | */ 138 | interface Persistor { 139 | pause(): void; 140 | persist(): void; 141 | purge(): Promise; 142 | flush(): Promise; 143 | dispatch(action: PersistorAction): PersistorAction; 144 | getState(): PersistorState; 145 | subscribe(callback: PersistorSubscribeCallback): () => any; 146 | } 147 | } 148 | 149 | declare module "redux-persist/lib/types" { 150 | export * from "redux-persist/es/types"; 151 | } 152 | --------------------------------------------------------------------------------