Predictable State at Enterprise Scale: Redux Toolkit, RTK Query & Optimistic Cache Synchronization
Taming State in High-Frequency React Applications
In complex frontend applications—such as real-time VoIP dialers, interactive canvas editors, and trading dashboards—uncontrolled state mutations lead to UI lag, race conditions, and unmaintainable code.
Redux Toolkit (RTK) paired with RTK Query provides an architecture for managing both global client-side UI state and synchronized server cache data.
1. Normalized State with createEntityAdapter
Instead of storing arrays of items that require $O(n)$ searches and expensive full-array updates, RTK's createEntityAdapter normalizes data into an { ids: [], entities: {} } lookup table:
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
export interface CallLog {
id: string;
callerNumber: string;
durationSeconds: number;
status: 'COMPLETED' | 'MISSED' | 'IN_PROGRESS';
timestamp: string;
}
export const callLogsAdapter = createEntityAdapter<CallLog>({
selectId: (log) => log.id,
sortComparer: (a, b) => b.timestamp.localeCompare(a.timestamp),
});
export const callLogsSlice = createSlice({
name: 'callLogs',
initialState: callLogsAdapter.getInitialState({ isDialing: false }),
reducers: {
addCallLog: callLogsAdapter.addOne,
updateCallStatus: callLogsAdapter.updateOne,
removeCallLog: callLogsAdapter.removeOne,
setDialingState(state, action) {
state.isDialing = action.payload;
},
},
});2. Optimistic UI Updates with RTK Query
For instant user feedback, RTK Query allows you to update the local cached state immediately before the network mutation finishes, automatically rolling back if the request fails:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const portfolioApi = createApi({
reducerPath: 'portfolioApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api/' }),
tagTypes: ['Projects'],
endpoints: (builder) => ({
toggleProjectStar: builder.mutation<{ success: boolean }, { id: number; isStarred: boolean }>({
query: ({ id, isStarred }) => ({
url: `projects/${id}/star`,
method: 'POST',
body: { isStarred },
}),
async onQueryStarted({ id, isStarred }, { dispatch, queryFulfilled }) {
// Optimistically update the UI cache
const patchResult = dispatch(
portfolioApi.util.updateQueryData('getProjects' as any, undefined, (draft: any) => {
const project = draft.find((p: any) => p.id === id);
if (project) project.is_starred = isStarred;
})
);
try {
await queryFulfilled;
} catch {
// Revert cache if server rejected
patchResult.undo();
}
},
}),
}),
});3. Real-Time WebSocket Redux Middleware
Connecting incoming WebSocket messages directly to Redux actions keeps the UI reactive while keeping components purely presentational:
import { Middleware } from '@reduxjs/toolkit';
import { callLogsSlice } from './call-logs-slice';
export const webSocketMiddleware: Middleware = (store) => {
let socket: WebSocket | null = null;
return (next) => (action: any) => {
if (action.type === 'ws/connect') {
socket = new WebSocket(action.payload.url);
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'CALL_UPDATE') {
store.dispatch(callLogsSlice.actions.updateCallStatus(message.data));
}
};
}
return next(action);
};
};4. Best Practice Summary
- Keep Component State Local: Only hoist state to Redux when it is shared across multiple disjoint subtrees.
- Use Normalized Entity Adapters: Prevents expensive re-renders and simplifies CRUD modifications.
- Use Optimistic Updates: Makes user actions feel instantaneous regardless of network latency.
Discussion (0)
Technical insights, critiques, and feedback
No comments yet. Be the first to start the discussion!