Production-Level Store Setup with TypeScript
Real-world Redux store configuration for large applications with proper TypeScript setup, middleware, and production optimizations.
Complete Store Setup
import { configureStore } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { rtkQueryErrorLogger } from './middleware/rtkQueryErrorLogger'
import { authSlice } from '../features/auth/authSlice'
import { userSlice } from '../features/user/userSlice'
import { productsApi } from '../features/products/productsApi'
import { ordersApi } from '../features/orders/ordersApi'
import { notificationSlice } from '../features/notifications/notificationSlice'
const authPersistConfig = {
key: 'auth',
storage,
whitelist: ['token', 'user', 'refreshToken']
}
const rootReducer = {
auth: persistReducer(authPersistConfig, authSlice.reducer),
user: userSlice.reducer,
notifications: notificationSlice.reducer,
productsApi: productsApi.reducer,
ordersApi: ordersApi.reducer,
}
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [
'persist/PERSIST',
'persist/REHYDRATE',
'persist/PAUSE',
'persist/PURGE',
'persist/REGISTER'
],
},
immutableCheck: import.meta.env.DEV,
})
.concat(productsApi.middleware)
.concat(ordersApi.middleware)
.concat(rtkQueryErrorLogger),
devTools: import.meta.env.DEV && {
maxAge: 50,
trace: true,
traceLimit: 25,
name: 'MyApp Redux Store'
},
preloadedState: undefined,
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const persistor = persistStore(store)
export default store
Why Each Part Matters
1. persistReducer - Keep Users Logged In
const authPersistConfig = {
key: 'auth', // Storage key in localStorage
storage, // Uses localStorage by default
whitelist: ['token', 'user', 'refreshToken'], // Only persist these fields
blacklist: ['isLoading', 'error'], // Never persist temporary states
// Transform data before saving/loading
transforms: [
{
in: (inboundState) => {
// Encrypt sensitive data before saving
return {
...inboundState,
token: encrypt(inboundState.token)
}
},
out: (outboundState) => {
// Decrypt when loading from storage
return {
...outboundState,
token: decrypt(outboundState.token)
}
}
}
],
// Migration for schema changes
version: 1,
migrate: createMigrate({
1: (state) => {
// Handle old auth structure
return {
...state,
user: state.userProfile || null // Rename userProfile to user
}
}
})
}
Why this matters in production:
- User Experience - Users don't need to login again after closing the browser
- Security - Only essential auth data is saved, sensitive temp data is excluded
- Performance - App starts faster by loading cached user data instead of API calls
- Offline Support - Basic user info available even without internet
- Schema Evolution - Migrations handle app updates gracefully
Common patterns:
// Different storage for different environments
const storage = import.meta.env.DEV
? require('redux-persist/lib/storage').default // localStorage in dev
: require('redux-persist/lib/storage/session').default // sessionStorage in prod
// Conditional persistence based on user preference
const authPersistConfig = {
key: 'auth',
storage: userWantsRememberMe ? localStorage : sessionStorage,
whitelist: ['token', 'user']
}
// Expire stored data after time
const authPersistConfig = {
key: 'auth',
storage,
whitelist: ['token', 'user'],
transforms: [
createTransform(
// Save with timestamp
(inboundState) => ({ ...inboundState, _timestamp: Date.now() }),
// Check if expired on load
(outboundState) => {
const now = Date.now()
const expires = outboundState._timestamp + (7 * 24 * 60 * 60 * 1000) // 7 days
return now < expires ? outboundState : {} // Clear if expired
}
)
]
}
2. Multiple API Slices - Separate Concerns
// Each domain gets its own API slice for better organization
productsApi: productsApi.reducer, // Product catalog, search, filters
ordersApi: ordersApi.reducer, // Order management, checkout
Keeps your API logic organized and prevents one huge API file.
3. Middleware Stack - Handle Side Effects
.concat(productsApi.middleware) // Enables RTK Query caching
.concat(ordersApi.middleware) // Handles background refetching
.concat(rtkQueryErrorLogger) // Custom error handling
4. Development vs Production - Performance Optimization (Vite)
immutableCheck: import.meta.env.DEV, // Skip checks in prod
devTools: import.meta.env.DEV && { // No DevTools in prod
maxAge: 50, // Limit memory usage
trace: true, // Debug action sources
name: 'MyApp Redux Store' // Store name in DevTools
}
Custom Error Middleware
// store/middleware/rtkQueryErrorLogger.ts
import type { Middleware } from '@reduxjs/toolkit'
import { isRejectedWithValue } from '@reduxjs/toolkit'
import { toast } from 'react-hot-toast'
export const rtkQueryErrorLogger: Middleware = () => (next) => (action) => {
// Check if this is a rejected RTK Query action
if (isRejectedWithValue(action)) {
const { payload, meta } = action
// Log error for debugging
console.error('RTK Query Error:', {
endpoint: meta.baseQueryMeta?.request?.url,
error: payload
})
// Handle different error types
if (payload.status === 401) {
toast.error('Please log in again')
// Could dispatch logout action here
} else if (payload.status === 403) {
toast.error('You don\'t have permission for this action')
} else if (payload.status >= 500) {
toast.error('Server error. Please try again later.')
} else {
toast.error(payload.data?.message || 'Something went wrong')
}
}
return next(action)
}
App Integration
// App.tsx
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import { store, persistor } from './store'
function App() {
return (
<Provider store={store}>
<PersistGate loading={<div>Loading...</div>} persistor={persistor}>
<Routes>
{/* Your app routes */}
</Routes>
</PersistGate>
</Provider>
)
}
Typed Hooks
// hooks/redux.ts
import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '../store'
// Use these instead of plain useDispatch and useSelector
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector = <T>(selector: (state: RootState) => T) =>
useSelector(selector)
Vite Environment Configuration
// store/config.ts
const isDevelopment = import.meta.env.DEV
const isProduction = import.meta.env.PROD
export const storeConfig = {
// More aggressive caching in production
keepUnusedDataFor: isProduction ? 300 : 60, // 5 min vs 1 min
// Disable some checks in production for performance
enableChecks: {
immutableCheck: isDevelopment,
serializableCheck: isDevelopment,
},
// API base URLs - Vite env variables
apiBaseUrl: import.meta.env.VITE_API_URL || 'http://localhost:3001/api',
// Feature flags
enableAnalytics: isProduction,
enableErrorReporting: isProduction,
// Vite-specific env variables
mode: import.meta.env.MODE, // 'development' | 'production' | 'test'
baseUrl: import.meta.env.BASE_URL, // Base URL for routing
}
Environment Variables (.env files)
# .env.development
VITE_API_URL=http://localhost:3001/api
VITE_ENABLE_DEVTOOLS=true
VITE_LOG_LEVEL=debug
# .env.production
VITE_API_URL=https://api.yourapp.com
VITE_ENABLE_DEVTOOLS=false
VITE_LOG_LEVEL=error
Vite Environment Types
// vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_ENABLE_DEVTOOLS: string
readonly VITE_LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error'
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Vite Hot Module Replacement (HMR)
// store/index.ts - Add this at the bottom for HMR
export const store = configureStore({
// ... your config
})
// Enable HMR for store in development
if (import.meta.env.DEV && import.meta.hot) {
import.meta.hot.accept('./rootReducer', () => {
// This enables hot reloading for reducers
const newRootReducer = require('./rootReducer').default
store.replaceReducer(newRootReducer)
})
}
Vite Build Optimizations
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
// Separate vendor chunks for better caching
vendor: ['react', 'react-dom', 'react-redux', '@reduxjs/toolkit'],
rtk: ['@reduxjs/toolkit/query']
}
}
}
},
optimizeDeps: {
include: ['react-redux', '@reduxjs/toolkit', '@reduxjs/toolkit/query']
}
})
Key Production Features
- Redux Persist - Saves auth state to localStorage
- Error Handling - Custom middleware for API errors
- Vite Environment Variables - Proper
import.meta.envusage - TypeScript - Full type safety throughout
- HMR Support - Hot reloading for development
- Modular - Separate API slices for different domains
- Middleware Stack - Handles caching, errors, persistence
- DevTools Config - Memory limits and debugging info
- Build Optimization - Code splitting and chunking
This setup is optimized for Vite + React with TypeScript, handles authentication persistence, error management, and scales well for large applications with multiple teams working on different features.