How to migrate from rematch to rtk #5192
Replies: 1 comment
-
|
Ah, Rematch. There's a name I haven't heard in a while :) There aren't any specific guides out there for migrating from Rematch, because it was never widely used in the first place. However, I'd recommend looking at the existing "Migrating to Modern Redux" docs page. That shows going from legacy hand-written Redux code to modern RTK. Not quite the same, but should give you the general ideas. Looking at the Rematch docs, it looks like a "model" is pretty close to Went ahead and threw this into Claude and it came up with this guide. Matches my understanding of Rematch's API: Rematch → RTK Migration GuideCore Concept Mapping
Example 1: Basic Model → SliceRematch: export const countModel = {
state: { counter: 0 },
reducers: {
add: (state, payload) => ({
...state,
counter: state.counter + payload,
}),
},
effects: {
async loadData(payload, rootState) {
const response = await fetch(`http://example.com/${payload}`)
const data = await response.json()
this.add(data) // dispatch local reducer
},
},
}RTK 2.x (with import { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit'
const createAppSlice = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator },
})
const countSlice = createAppSlice({
name: 'count',
initialState: { counter: 0 },
reducers: (create) => ({
add: create.reducer<number>((state, action) => {
state.counter += action.payload
}),
loadData: create.asyncThunk(
async (payload: string, thunkApi) => {
const response = await fetch(`http://example.com/${payload}`)
return response.json()
},
{
fulfilled: (state, action) => {
state.counter += action.payload
},
}
),
}),
})
export const { add, loadData } = countSlice.actionsRTK (classic approach with separate thunk): import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
export const loadData = createAsyncThunk(
'count/loadData',
async (payload: string) => {
const response = await fetch(`http://example.com/${payload}`)
return response.json()
}
)
const countSlice = createSlice({
name: 'count',
initialState: { counter: 0 },
reducers: {
add: (state, action) => {
state.counter += action.payload
},
},
extraReducers: (builder) => {
builder.addCase(loadData.fulfilled, (state, action) => {
state.counter += action.payload
})
},
})Example 2: Effects That Dispatch Other ModelsRematch: effects: (dispatch) => ({
async triggerData(payload, rootState) {
console.log(rootState.example)
await dispatch.count.loadData(payload) // cross-model dispatch
},
}),RTK: export const triggerData = createAsyncThunk(
'example/triggerData',
async (payload: string, { getState, dispatch }) => {
const state = getState() as RootState
console.log(state.example)
await dispatch(loadData(payload)) // just dispatch the thunk
}
)Key difference: RTK thunks receive Example 3: Cross-Model Reducers (listening to other slices)Rematch supports listening via string keys like reducers: {
'user/logout': (state) => initialState, // reset on logout
}RTK: // In the other slice:
export const logout = createAction('user/logout')
// In your slice:
const mySlice = createSlice({
name: 'myModel',
initialState,
reducers: { /* ... */ },
extraReducers: (builder) => {
builder.addCase(logout, () => initialState)
},
})Or with string matching: extraReducers: (builder) => {
builder.addMatcher(
(action) => action.type === 'user/logout',
() => initialState
)
}Plugin Migration
|
| Rematch Plugin Hook | RTK Equivalent |
|---|---|
createMiddleware |
configureStore({ middleware: (getDefault) => getDefault().concat(myMiddleware) }) |
config.redux.middlewares |
Same as above |
onRootReducer (wrap reducer) |
Wrap reducer before passing to configureStore, or use combineSlices |
onStoreCreated (add store props) |
Just add properties to store after creation, or use enhancers |
config.redux.enhancers |
configureStore({ enhancers: (getDefault) => getDefault().concat(myEnhancer) }) |
exposed (cross-plugin state) |
Not needed—just use module-level variables or store properties |
Example: Rematch plugin that adds middleware + store method
// Rematch
const myPlugin = {
createMiddleware: (bag) => (store) => (next) => (action) => {
console.log('action:', action.type)
return next(action)
},
onStoreCreated(store) {
store.customMethod = () => console.log('hello')
return store
},
}// RTK
const loggingMiddleware: Middleware = (store) => (next) => (action) => {
console.log('action:', action.type)
return next(action)
}
const store = configureStore({
reducer: rootReducer,
middleware: (getDefault) => getDefault().concat(loggingMiddleware),
})
// Add custom method directly
store.customMethod = () => console.log('hello')Example: Wrapping the root reducer (like onRootReducer)
// For things like redux-persist or reset-on-logout
const rootReducer = combineReducers({ /* slices */ })
const wrappedReducer: typeof rootReducer = (state, action) => {
if (action.type === 'RESET') {
return rootReducer(undefined, action)
}
return rootReducer(state, action)
}
const store = configureStore({ reducer: wrappedReducer })The general philosophy: RTK exposes Redux's extension points directly rather than abstracting them behind a plugin system. Most Rematch plugins were thin wrappers anyway.
Effects → Listener Middleware (for Reactive Logic)
Rematch effects map to thunks for user-initiated async work. But some effects are more "reactive"—they respond to other actions or state changes. RTK's listener middleware is often a better fit for these.
When to use what:
| Pattern | Use |
|---|---|
| User clicks button → fetch data → update state | Thunk (createAsyncThunk) |
| When action X fires, also do Y | Listener middleware |
| When state.foo changes, trigger side effect | Listener middleware |
| Complex async workflows with cancellation | Listener middleware |
Example: Effect that reacts to another action
// Rematch - effect that fires when something else happens
effects: (dispatch) => ({
async onUserLogin(payload, rootState) {
// fetch user preferences after login
const prefs = await fetchPreferences(payload.userId)
dispatch.preferences.set(prefs)
},
}),
// Called manually: dispatch.auth.onUserLogin(user)// RTK Listener - automatically reacts to login action
import { createListenerMiddleware } from '@reduxjs/toolkit'
const listenerMiddleware = createListenerMiddleware()
listenerMiddleware.startListening({
actionCreator: loginSuccess, // from auth slice
effect: async (action, listenerApi) => {
const prefs = await fetchPreferences(action.payload.userId)
listenerApi.dispatch(preferencesSlice.actions.set(prefs))
},
})
// Add to store
const store = configureStore({
reducer: rootReducer,
middleware: (getDefault) => getDefault().prepend(listenerMiddleware.middleware),
})Example: Effect that watches state changes
listenerMiddleware.startListening({
predicate: (action, currentState, previousState) => {
return currentState.cart.items.length !== previousState.cart.items.length
},
effect: async (action, listenerApi) => {
// Sync cart to server whenever items change
await syncCartToServer(listenerApi.getState().cart)
},
})Example: Cancellable effect (listener advantage)
listenerMiddleware.startListening({
actionCreator: startPolling,
effect: async (action, listenerApi) => {
while (true) {
const data = await fetchLatestData()
listenerApi.dispatch(dataReceived(data))
// Wait 5s, but cancel if stopPolling fires
const result = await listenerApi.condition(stopPolling.match, 5000)
if (result) break // stopPolling was dispatched
}
},
})This kind of cancellation-aware async logic is awkward with thunks but natural with listeners.
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
is their any guide for this?
Beta Was this translation helpful? Give feedback.
All reactions