Skip to content

Migrations with Redux Remigrate

As your application evolves, your persisted state schema will change. Redux Remigrate is a TypeScript-first migration layer that provides type-safe, automatic state migrations management for Redux Remember.

Without migrations, users with old persisted data may experience:

  • Runtime errors when accessing renamed or removed fields
  • Loss of data when state shape changes
  • Broken application state after updates

Redux Remigrate solves this by:

  • TypeScript-first approach - Auto-generated version types ensure type safety
  • Automatic version tracking - Tracks schema versions and runs migrations sequentially
  • CLI tooling - Generate migrations, validate types, and clean up unused files
  • Circular migration detection - Built-in safeguards prevent infinite loops

Install Redux Remigrate alongside Redux Remember:

Terminal window
npm install redux-remigrate

Create remigrate.config.ts in your project root:

import { defineRemigrateConfig } from 'redux-remigrate';
export default defineRemigrateConfig({
storagePath: './src/remigrate',
stateFilePath: './src/store.ts',
stateTypeExpression: 'PersistedState',
});

See Configuration Options for all available settings.

Import the generated migrate function and pass it to Redux Remember:

import { configureStore } from '@reduxjs/toolkit';
import { rememberReducer, rememberEnhancer } from 'redux-remember';
import { _remigrateVersion } from 'redux-remigrate';
import { myStateIsRemembered, myStateIsForgotten } from './reducers.ts';
import { migrate } from './remigrate/index.ts';
const reducers = {
// 1. Add the _remigrateVersion reducer to track your state version:
_remigrateVersion,
myStateIsForgotten,
myStateIsRemembered,
};
const rememberedKeys = [
// 2. Make sure _remigrateVersion gets persisted with the state:
'_remigrateVersion',
'myStateIsRemembered',
] satisfies (keyof typeof reducers)[];
const reducer = rememberReducer(reducers);
const store = configureStore({
reducer,
enhancers: (getDefaultEnhancers) => getDefaultEnhancers().concat(
rememberEnhancer(window.localStorage, rememberedKeys, {
// 3. Pass the migrate function to Redux Remember:
migrate
})
)
});
export type RootState = ReturnType<typeof store['getState']>;
// 4. Make sure to set config value of "stateTypeExpression" to "PersistedState"
export type PersistedState = Pick<RootState, typeof rememberedKeys[number]>;
Terminal window
npx remigrate init

This creates the migrations directory structure and generates the initial version file based on your current store type.

4.1. Whenever you modify the structure of your persisted state, run:

Section titled “4.1. Whenever you modify the structure of your persisted state, run:”
Terminal window
npx remigrate create

This will:

  1. Detect changes to your store type
  2. Generate a new version file if your persisted state is changed
  3. Create a new migration file with type-safe function signatures

Edit the generated migration file to transform data from the old version to the new:

import type { StoreVersion_20260101_120000 } from '../versions/20260101_120000.ts';
import type { StoreVersion_20260115_140000 } from '../versions/20260115_140000.ts';
export const from_20260101_120000 = (
{ myStateIsRemembered, ...store }: StoreVersion_20260101_120000
): StoreVersion_20260115_140000 => ({
...store,
myStateIsRemembered: {
...myStateIsRemembered,
newFieldAdded: 'default value'
},
_remigrateVersion: '20260115_140000',
});

Check your migrations for TypeScript errors:

Terminal window
npx remigrate validate

This ensures all migration functions have correct type signatures.

When the app loads and persisted data is rehydrated, Redux Remigrate automatically migrates it to the latest schema version.

Usage: remigrate <command> [options]
Commands:
init [suffix] Initialize migrations directory
create [suffix] Create a new migration file if store type changed
cleanup Remove unreferenced version type files
validate Validate migrations for TypeScript errors
Options:
-c, --config <file> Override config file path

Initializes the migrations directory structure. Run this once when setting up migrations.

Terminal window
npx remigrate init

The optional [suffix] argument adds a suffix to the generated version file name.

Creates a new migration when your persisted state type changes.

Terminal window
npx remigrate create

If the type hasn’t changed, the command will skip creation. The optional [suffix] argument adds a descriptive suffix to the generated version files.

Removes unreferenced version type files that are no longer used by any migration.

Terminal window
npx remigrate cleanup

Validates all migration files for TypeScript errors.

Terminal window
npx remigrate validate
OptionTypeRequiredDescription
storagePathstringYesDirectory where migrations will be stored
stateFilePathstringYesPath to the file containing your persisted state type
stateTypeExpressionstringYesThe TypeScript type expression for your persisted state
prettierrcPathstringNoPath to your Prettier config file
tsconfigPathstringNoPath to your TypeScript config file
headers.versionFilestringNoCustom header for generated version files
headers.migrationFilestringNoCustom header for generated migration files
headers.indexFilestringNoCustom header for the generated index file
import { defineRemigrateConfig } from 'redux-remigrate';
export default defineRemigrateConfig({
storagePath: './src/remigrate',
stateFilePath: './src/store.ts',
stateTypeExpression: 'PersistedState',
prettierrcPath: './.prettierrc',
tsconfigPath: './tsconfig.json',
headers: {
versionFile: '/* eslint-disable */',
migrationFile: '/* eslint-disable */',
indexFile: '/* eslint-disable */',
},
});

After initialization, Redux Remigrate creates the following structure:

src/remigrate/
├── index.ts # Exports the migrate function
├── versions/ # Version type snapshots
│ └── 20260101_120000.ts
└── migrations/ # Migration functions

Each time you run npx remigrate create, new files are added:

src/remigrate/
├── index.ts # Updated with new version
├── versions/
│ ├── 20260101_120000.ts
│ └── 20260115_140000.ts # New version snapshot
└── migrations/
└── 20260115_140000.ts # Migration from previous version
export const from_20260101_120000 = (
state: StoreVersion_20260101_120000
): StoreVersion_20260115_140000 => ({
...state,
mySlice: {
...state.mySlice,
newField: 'default value'
},
_remigrateVersion: '20260115_140000',
});
export const from_20260101_120000 = (
{ mySlice, ...state }: StoreVersion_20260101_120000
): StoreVersion_20260115_140000 => {
const { oldFieldName, ...rest } = mySlice;
return {
...state,
mySlice: {
...rest,
newFieldName: oldFieldName
},
_remigrateVersion: '20260115_140000',
};
};
export const from_20260101_120000 = (
{ mySlice, ...state }: StoreVersion_20260101_120000
): StoreVersion_20260115_140000 => {
const { removedField, ...rest } = mySlice;
return {
...state,
mySlice: rest,
_remigrateVersion: '20260115_140000',
};
};
export const from_20260101_120000 = (
state: StoreVersion_20260101_120000
): StoreVersion_20260115_140000 => ({
...state,
mySlice: {
...state.mySlice,
tags: state.mySlice.tags
? state.mySlice.tags.split(',') // Convert existing string to an array
: []
},
_remigrateVersion: '20260115_140000',
});

If a migration throws an error, Redux Remember’s errorHandler will receive it as a MigrateError, and the default store state (from reducer initial values) is used to prevent the app from crashing. Always test your migrations thoroughly.

Note: Migration only runs after successful rehydration. If rehydration fails, migration is skipped entirely.

import { migrate } from './remigrate/index.ts';
import { MigrateError } from 'redux-remember';
rememberEnhancer(window.localStorage, rememberedKeys, {
migrate,
errorHandler: (error) => {
if (error instanceof MigrateError) {
console.error('Migration failed:', error);
// Default store state will be used - consider notifying the user
}
}
})