Created
August 25, 2020 16:32
-
-
Save aguynamedben/3bef9af36c2a257aa2f0c77e3f046d01 to your computer and use it in GitHub Desktop.
Persist Redux to localStorage, with migrations (i.e. migrate off redux-persist)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @flow | |
// Inspired by https://medium.com/@jrcreencia/persisting-redux-state-to-local-storage-f81eb0b90e7e | |
const log = console; | |
let firstLoad = true; | |
export function loadState() { | |
try { | |
const serializedState = localStorage.getItem('state'); | |
if (!serializedState) { | |
return undefined; | |
} | |
const state = JSON.parse(serializedState); | |
if (firstLoad) { | |
log.debug(`loaded state from localStorage for the first time`, state); | |
firstLoad = false; | |
} else { | |
log.warn(`reloaded state from localStorage`, state); | |
} | |
return state; | |
} catch (error) { | |
log.error(`couldn't load state from localStorage: ${error.toString()}`); | |
return undefined; | |
} | |
} | |
// TODO: use type for state, when we use createSlice | |
export function saveState(state: any) { | |
try { | |
const serializedState = JSON.stringify(state); | |
localStorage.setItem('state', serializedState); | |
log.debug(`saved state to localStorage`, state); | |
} catch (error) { | |
// ignore write errors | |
log.error(`couldn't save state to localStorage: ${error.toString()}`); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// You only need this file if you're migrating from redux-persist to this solution | |
import * as logger from 'shared/logger'; | |
import { saveState } from 'foreground/localStorage'; | |
const log = logger.make('migrateReduxPersist'); | |
export function migrateReduxPersistIfNecessary() { | |
const serializedReduxPersistState = localStorage.getItem('persist:root'); | |
const serializedState = localStorage.getItem('state'); | |
if (serializedState !== null) { | |
log.info(`not moving off redux-persist: 'state' already present in localStorage`); | |
return; | |
} | |
if (serializedReduxPersistState == null) { | |
log.info(`not moving off redux-persist: 'persist:root' not present in localStorage`); | |
return; | |
} | |
log.info(`moving off redux-persist`); | |
let state; | |
try { | |
state = deserializeReduxPersistState(serializedReduxPersistState); | |
} catch (error) { | |
// this should never happen, but if it does it's better to reset redux state | |
// than leave it for-sure broken | |
console.warn( | |
`Couldn't move off redux-persist due to problem deserializing 'persist:root' in localStorage. This will reset the user's redux state.`, | |
error, | |
); | |
return; | |
} | |
localStorage.setItem('stateVersion', state._persist.version); | |
delete state._persist; | |
saveState(state); | |
localStorage.removeItem('persist:root'); | |
log.info(`done moving off redux-persist`); | |
} | |
// redux-persist serialization calls JSON.parses at each level of state | |
// mimic https://github.com/rt2zz/redux-persist/blob/c3841f2fc56adf30a87cd61f7d2315146048079e/src/getStoredState.js | |
function deserializeReduxPersistState(serialized) { | |
const transforms = []; | |
try { | |
const state = {}; | |
const rawState = deserialize(serialized); | |
Object.keys(rawState).forEach((key) => { | |
state[key] = transforms.reduceRight((subState, transformer) => { | |
return transformer.out(subState, key, rawState); | |
}, deserialize(rawState[key])); | |
}); | |
return state; | |
} catch (error) { | |
console.warn(`error restoring redux-persist data ${serialized}`, error); | |
throw error; | |
} | |
} | |
function deserialize(serial) { | |
return JSON.parse(serial); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import preduce from 'p-reduce'; | |
import * as logger from 'shared/logger'; | |
import { loadState, saveState } from 'foreground/localStorage'; | |
import { migrations, targetVersion } from './migrations'; | |
const log = logger.make('reduxMigrations'); | |
export async function loadAndMigrateState() { | |
const state = loadState(); | |
const stateVersion = parseInt(localStorage.getItem('stateVersion'), 10); | |
if (!state || Number.isNaN(stateVersion)) { | |
log.info(`state or stateVersion not present in localStorage, state will be new`); | |
saveState(undefined); | |
localStorage.setItem('stateVersion', targetVersion); | |
return undefined; | |
} | |
log.info(`stateVersion: ${stateVersion}, targetVersion: ${targetVersion}`); | |
if (stateVersion === targetVersion) { | |
log.info(`already caught up to migration ${targetVersion}, no migrations to run`); | |
return state; | |
} | |
if (stateVersion > targetVersion) { | |
log.warn(`downgrading version is not supported`); | |
return state; | |
} | |
const migrationIds = Object.keys(migrations) | |
.filter((migrationId) => ( | |
migrationId <= targetVersion && migrationId > stateVersion | |
)) | |
.sort((a, b) => a - b); | |
log.info(`running migrations ${migrationIds.join(', ')}`); | |
return preduce(migrationIds, applyMigration, state); | |
} | |
async function applyMigration(state, version) { | |
log.info(`running migration ${version}`); | |
// doing this lets you run non-async migrations too | |
return Promise.resolve(migrations[version](state)) | |
.then((nextState) => { | |
log.info(`successfully ran migration ${version}`); | |
saveState(nextState); | |
localStorage.setItem('stateVersion', version); | |
return nextState; | |
}) | |
.catch((error) => { | |
log.info(`error running migration ${version}`, error.message); | |
return Promise.reject(error); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// An example migration | |
import log from 'shared/logger'; | |
/** | |
* No-op for the first migration... because okay! | |
*/ | |
export default async function migration1(state) { | |
log.debug(`migration1 called with state`, state); | |
return state; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import migration1 from './migration1'; | |
import migration2 from './migration2'; | |
import migration3 from './migration3'; | |
import migration4 from './migration4'; | |
import migration5 from './migration5'; | |
import migration6 from './migration6'; | |
import migration7 from './migration7'; | |
import migration8 from './migration8'; | |
import migration9 from './migration9'; | |
import migration10 from './migration10'; | |
import migration11 from './migration11'; | |
import migration12 from './migration12'; | |
import migration13 from './migration13'; | |
import migration14 from './migration14'; | |
import migration15 from './migration15'; | |
import migration16 from './migration16'; | |
import migration17 from './migration17'; | |
import migration18 from './migration18'; | |
import migration19 from './migration19'; | |
import migration20 from './migration20'; | |
import migration21 from './migration21'; | |
import migration22 from './migration22'; | |
import migration23 from './migration23'; | |
import migration24 from './migration24'; | |
import migration25 from './migration25'; | |
import migration26 from './migration26'; | |
import migration27 from './migration27'; | |
// Update this when you add a new migration! | |
export const targetVersion = 27; | |
export const migrations = { | |
1: migration1, | |
2: migration2, | |
3: migration3, | |
4: migration4, | |
5: migration5, | |
6: migration6, | |
7: migration7, | |
8: migration8, | |
9: migration9, | |
10: migration10, | |
11: migration11, | |
12: migration12, | |
13: migration13, | |
14: migration14, | |
15: migration15, | |
16: migration16, | |
17: migration17, | |
18: migration18, | |
19: migration19, | |
20: migration20, | |
21: migration21, | |
22: migration22, | |
23: migration23, | |
24: migration24, | |
25: migration25, | |
26: migration26, | |
27: migration27, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// USAGE | |
// in store.js... | |
migrateReduxPersistIfNecessary(); | |
const state = await loadAndMigrateState(); | |
const store = createStore(rootReducer, state, enhancer); | |
store.subscribe(_.throttle(() => { | |
saveState(store.getState()); | |
}, 1000)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note: Where there's a hyphen (
-
) in a filename (i.e.reduxMigrations-migrations.js
) the hyphen should be a/
. Gist doesn't allow directories in Gist file paths.