Last active
February 13, 2024 03:42
-
-
Save lafiosca/b7bbb569ae3fe5c1ce110bf71d7ee153 to your computer and use it in GitHub Desktop.
redux-persist migrations in TypeScript with a little help from patch-package
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 { createMigrate } from 'redux-persist'; | |
import { RootState } from './modules/reducer'; | |
/* | |
* Latest version (V3) is simply the currently used redux RootState. | |
*/ | |
type PersistedRootStateV3 = RootState; | |
/* | |
* Previous versions can be built from advanced types, working backward from current; | |
* in this example a field was added at `slice.newField` when going from V2 to V3, | |
* so to get V2's shape we take everything except that field from V3. | |
*/ | |
type PersistedRootStateV2 = Omit<PersistedRootStateV3, 'slice'> & { | |
slice: Omit<PersistedRootStateV2['slice'], 'newField'>; | |
}; | |
/* | |
* The entire `slice` reducer was added when going from V1 to V2, so omit it from the | |
* V2 definition. This approach of building incrementally backwards also means that we | |
* don't have to update all of the old state types each time we add a new migration. | |
*/ | |
type PersistedRootStateV1 = Omit<PersistedRootStateV2, 'slice'>; | |
/* | |
* Each migration step will take one version as input and return the next version as output. | |
* (The key `2` means that it is the step which migrates from V1 to V2.) | |
*/ | |
const persistMigrations = { | |
2: (state: PersistedRootStateV1): PersistedRootStateV2 => ({ | |
...state, | |
slice: { // Introduce new `slice`. | |
someField: 0, | |
someOtherField: [], | |
}, | |
}), | |
3: async (state: PersistedRootStateV2): Promise<PersistedRootStateV3> => { | |
return { | |
...state, | |
slice: { | |
...state.slice, | |
newField: {}, // Add `newField` to `slice`. | |
}, | |
}; | |
}, | |
}; | |
/* | |
* A union type is created specifically to use below. | |
*/ | |
type MigrationState = PersistedRootStateV1 | PersistedRootStateV2 | PersistedRootStateV3 | |
/* | |
* The union type is used as the generic for `createMigrate`. The type must be explicitly | |
* provided. Relying on type inference here would fail because it would assume {}. | |
*/ | |
export const persistMigrate = createMigrate<MigrationState>(persistMigrations); | |
/* | |
* This is the current version and should match the latest version above (V3). | |
*/ | |
export const persistVersion = 3; |
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
diff --git a/node_modules/redux-persist/types/createMigrate.d.ts b/node_modules/redux-persist/types/createMigrate.d.ts | |
index 2c1329d..2e4cd57 100644 | |
--- a/node_modules/redux-persist/types/createMigrate.d.ts | |
+++ b/node_modules/redux-persist/types/createMigrate.d.ts | |
@@ -11,7 +11,7 @@ declare module "redux-persist/es/createMigrate" { | |
* @param config migration configuration | |
*/ | |
// tslint:disable-next-line: strict-export-declare-modifiers | |
- export default function createMigrate(migrations: MigrationManifest, config?: MigrationConfig): PersistMigrate; | |
+ export default function createMigrate<T extends {}>(migrations: MigrationManifest<T>, config?: MigrationConfig): PersistMigrate; | |
} | |
declare module "redux-persist/lib/createMigrate" { | |
diff --git a/node_modules/redux-persist/types/types.d.ts b/node_modules/redux-persist/types/types.d.ts | |
index b3733bc..f8106cb 100644 | |
--- a/node_modules/redux-persist/types/types.d.ts | |
+++ b/node_modules/redux-persist/types/types.d.ts | |
@@ -72,9 +72,9 @@ declare module "redux-persist/es/types" { | |
removeItem(key: string): Promise<void>; | |
} | |
- interface MigrationManifest { | |
- [key: string]: (state: PersistedState) => PersistedState; | |
- } | |
+ type MigrationFunction<T extends {}, U extends {} = T> = T extends infer V ? U extends infer W ? (state: V) => W | Promise<W> : never : never; | |
+ | |
+ type MigrationManifest<T extends {}> = Record<string, MigrationFunction<T>>; | |
/** | |
* @desc |
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 { persistReducer } from 'redux-persist'; | |
import AsyncStorage from '@react-native-community/async-storage'; | |
import { persistVersion, persistMigrate } from './persistMigrations'; | |
/* ... */ | |
const persistConfig = { | |
key: 'root', | |
storage: AsyncStorage, | |
whitelist: [ | |
'slice', | |
/* ... */ | |
], | |
version: persistVersion, | |
migrate: persistMigrate, | |
}; | |
const persistedRootReducer = persistReducer(persistConfig, rootReducer); | |
/* ... and so on, creating the store as usual */ |
Over time I've actually found it easier to just wipe the entire store when I have breaking migrations. Perhaps not the smoothest user experience, but less bookkeeping:
import { createMigrate } from 'redux-persist';
import { RootState } from './modules/reducer';
import { resetStore } from './modules/actions';
import { dispatchLogMessage } from '../services/log';
import { somethingReducer } from './modules/something/reducer';
import { somethingElseReducer } from './modules/somethingElse/reducer';
export const persistVersion = 13;
export const persistWhitelist = [
'something',
'somethingElse',
] as const;
type Whitelist = typeof persistWhitelist;
type Whitelisted = Whitelist extends ReadonlyArray<infer T> ? T : never;
type PersistedRootState = Pick<RootState, Whitelisted>;
const persistMigrations = {
[persistVersion]: (): PersistedRootState => {
dispatchLogMessage('Resetting redux-persist store');
const action = resetStore();
return {
something: somethingReducer(undefined, action),
somethingElse: somethingElseReducer(undefined, action),
};
},
};
export const persistMigrate = createMigrate<PersistedRootState>(persistMigrations);
All I do is bump the version number.
Over time I've actually found it easier to just wipe the entire store when I have breaking migrations. Perhaps not the smoothest user experience, but less bookkeeping
I'm at a point where I'm forced to agree 😅
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This was helpful to me. Any idea why this isn't in
redux-persist
already?Edit
I see you have been discussing things here! rt2zz/redux-persist#1065 (comment)