Day 10: Real-World Patterns
What You'll Learn Today
- Authentication pattern (login flow, token storage, protected routes)
- Feature-based project structure
- Redux DevTools for debugging
- Migration guide from React Context to Redux
- When NOT to use Redux
- Code splitting with
combineSlices - Error boundary integration
- 10-day journey recap and next steps
Authentication Pattern
Almost every web application needs authentication. Here is a robust pattern for handling it with Redux.
The authSlice Implementation
// features/auth/authSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const login = createAsyncThunk(
'auth/login',
async ({ email, password }, { rejectWithValue }) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const error = await response.json();
return rejectWithValue(error.message);
}
const data = await response.json();
localStorage.setItem('token', data.token);
return data;
} catch (error) {
return rejectWithValue('Network error');
}
}
);
export const logout = createAsyncThunk(
'auth/logout',
async () => {
localStorage.removeItem('token');
return null;
}
);
export const checkAuth = createAsyncThunk(
'auth/checkAuth',
async (_, { rejectWithValue }) => {
const token = localStorage.getItem('token');
if (!token) {
return rejectWithValue('No token');
}
try {
const response = await fetch('/api/auth/me', {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
localStorage.removeItem('token');
return rejectWithValue('Invalid token');
}
return await response.json();
} catch (error) {
return rejectWithValue('Network error');
}
}
);
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
token: null,
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
reducers: {
clearError(state) {
state.error = null;
},
},
extraReducers: (builder) => {
builder
// Login
.addCase(login.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(login.fulfilled, (state, action) => {
state.status = 'succeeded';
state.user = action.payload.user;
state.token = action.payload.token;
})
.addCase(login.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
})
// Logout
.addCase(logout.fulfilled, (state) => {
state.user = null;
state.token = null;
state.status = 'idle';
})
// Check Auth
.addCase(checkAuth.fulfilled, (state, action) => {
state.user = action.payload;
state.status = 'succeeded';
})
.addCase(checkAuth.rejected, (state) => {
state.user = null;
state.token = null;
state.status = 'idle';
});
},
});
export const { clearError } = authSlice.actions;
export default authSlice.reducer;
TypeScript version
// features/auth/authSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
interface User {
id: string;
email: string;
name: string;
}
interface AuthState {
user: User | null;
token: string | null;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
}
interface LoginCredentials {
email: string;
password: string;
}
interface LoginResponse {
user: User;
token: string;
}
export const login = createAsyncThunk<
LoginResponse,
LoginCredentials,
{ rejectValue: string }
>(
'auth/login',
async ({ email, password }, { rejectWithValue }) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const error = await response.json();
return rejectWithValue(error.message);
}
const data: LoginResponse = await response.json();
localStorage.setItem('token', data.token);
return data;
} catch {
return rejectWithValue('Network error');
}
}
);
const initialState: AuthState = {
user: null,
token: null,
status: 'idle',
error: null,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
clearError(state) {
state.error = null;
},
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => {
state.status = 'loading';
state.error = null;
})
.addCase(login.fulfilled, (state, action) => {
state.status = 'succeeded';
state.user = action.payload.user;
state.token = action.payload.token;
})
.addCase(login.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload ?? 'Unknown error';
});
},
});
export const { clearError } = authSlice.actions;
export default authSlice.reducer;
Protected Routes
// components/ProtectedRoute.jsx
import React from 'react';
import { useSelector } from 'react-redux';
import { Navigate, useLocation } from 'react-router-dom';
function ProtectedRoute({ children }) {
const { user, status } = useSelector((state) => state.auth);
const location = useLocation();
if (status === 'loading') {
return <div>Loading...</div>;
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
export default ProtectedRoute;
// App.jsx - Routing setup
import { Routes, Route } from 'react-router-dom';
import ProtectedRoute from './components/ProtectedRoute';
import LoginPage from './pages/LoginPage';
import Dashboard from './pages/Dashboard';
function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>
);
}
Feature-Based Project Structure
For medium to large Redux applications, organizing code by feature dramatically improves maintainability and discoverability.
graph TB
subgraph Project["Recommended Project Structure"]
SRC["src/"]
APP["app/<br/>store.js, hooks.js"]
FEAT["features/"]
AUTH["auth/<br/>authSlice.js<br/>authApi.js<br/>LoginForm.jsx<br/>AuthGuard.jsx"]
POSTS["posts/<br/>postsSlice.js<br/>postsApi.js<br/>PostList.jsx<br/>PostDetail.jsx"]
USERS["users/<br/>usersSlice.js<br/>usersApi.js<br/>UserProfile.jsx"]
COMP["components/<br/>Header.jsx<br/>Layout.jsx<br/>Button.jsx"]
end
SRC --> APP
SRC --> FEAT
SRC --> COMP
FEAT --> AUTH
FEAT --> POSTS
FEAT --> USERS
style Project fill:transparent,stroke:#666
style APP fill:#3b82f6,color:#fff
style AUTH fill:#8b5cf6,color:#fff
style POSTS fill:#8b5cf6,color:#fff
style USERS fill:#8b5cf6,color:#fff
style COMP fill:#22c55e,color:#fff
Detailed Directory Layout
src/
βββ app/
β βββ store.js # Store configuration
β βββ hooks.js # Typed hooks (useAppDispatch, useAppSelector)
β βββ rootReducer.js # Combine all reducers
βββ features/
β βββ auth/
β β βββ authSlice.js
β β βββ authApi.js # RTK Query or async thunks
β β βββ LoginForm.jsx
β β βββ AuthGuard.jsx
β β βββ auth.test.js
β βββ posts/
β β βββ postsSlice.js
β β βββ postsApi.js
β β βββ PostList.jsx
β β βββ PostDetail.jsx
β β βββ posts.test.js
β βββ users/
β βββ usersSlice.js
β βββ usersApi.js
β βββ UserProfile.jsx
β βββ users.test.js
βββ components/ # Shared/presentational components
β βββ Header.jsx
β βββ Layout.jsx
β βββ Button.jsx
βββ pages/ # Route-level components
β βββ HomePage.jsx
β βββ LoginPage.jsx
β βββ DashboardPage.jsx
βββ utils/ # Shared utilities
βββ test-utils.js
βββ api.js
Benefits of this structure:
- Related code lives together in the same directory
- Adding a new feature is straightforward (just create a new directory)
- Test files sit next to the code they test
- Removing a feature is as simple as deleting a directory
Redux DevTools
Redux DevTools is the most powerful debugging tool in the Redux ecosystem.
Key Features
1. Action Log
View every dispatched action in chronological order. For each action, you can inspect the type, payload, and resulting state.
2. State Diff
See exactly which parts of state changed for each action. This makes it easy to spot unintended mutations.
3. Time Travel
Rewind state to any previous point or skip specific actions. Invaluable for tracking down the root cause of bugs.
4. State Import/Export
Export the current state as JSON and import it later. Perfect for attaching state snapshots to bug reports.
Configuration
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: {
// reducers...
},
// DevTools is enabled by default in development
devTools: process.env.NODE_ENV !== 'production',
});
Note: When using
configureStore, DevTools is automatically enabled in development. No additional setup is required.
Customizing DevTools
const store = configureStore({
reducer: rootReducer,
devTools: {
name: 'My App',
trace: true, // Action dispatch stack trace
traceLimit: 25, // Stack trace depth limit
maxAge: 50, // Max stored actions
},
});
Migrating from React Context to Redux
A step-by-step guide for migrating existing Context-based state management to Redux.
Step 1: Assess the Current Code
// Before: Context-based state management
const AuthContext = React.createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const login = async (email, password) => {
setLoading(true);
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await res.json();
setUser(data.user);
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
localStorage.removeItem('token');
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
Step 2: Create the Slice
// After: Redux slice (same logic, different structure)
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const loginUser = createAsyncThunk(
'auth/login',
async ({ email, password }) => {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
return await res.json();
}
);
const authSlice = createSlice({
name: 'auth',
initialState: { user: null, loading: false },
reducers: {
logoutUser(state) {
state.user = null;
localStorage.removeItem('token');
},
},
extraReducers: (builder) => {
builder
.addCase(loginUser.pending, (state) => {
state.loading = true;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload.user;
})
.addCase(loginUser.rejected, (state) => {
state.loading = false;
});
},
});
Step 3: Migrate Components Incrementally
// Transitional: Support both Context and Redux
function useAuth() {
// Phase 1: Still using Context
// return useContext(AuthContext);
// Phase 2: Using Redux
const dispatch = useDispatch();
const { user, loading } = useSelector((state) => state.auth);
return {
user,
loading,
login: (email, password) => dispatch(loginUser({ email, password })),
logout: () => dispatch(logoutUser()),
};
}
Migration tips:
- Abstracting behind custom hooks makes switching the implementation painless
- You don't have to migrate everything at once -- go feature by feature
- Write tests first to ensure behavior stays the same after migration
When NOT to Use Redux
Redux is not the right tool for every kind of state. Use this decision guide.
flowchart TD
START["Need state management"] --> Q1{"Accessed by<br/>multiple components?"}
Q1 -- "No" --> LOCAL["Local State<br/>useState / useReducer"]
Q1 -- "Yes" --> Q2{"Server data?"}
Q2 -- "Yes" --> Q3{"Need caching<br/>& revalidation?"}
Q3 -- "Yes" --> SERVER["Server State<br/>RTK Query / React Query<br/>/ SWR"]
Q3 -- "No" --> REDUX["Redux"]
Q2 -- "No" --> Q4{"Should live in URL?"}
Q4 -- "Yes" --> URL["URL State<br/>React Router<br/>/ searchParams"]
Q4 -- "No" --> Q5{"Complex logic<br/>or action history?"}
Q5 -- "Yes" --> REDUX
Q5 -- "No" --> CONTEXT["React Context"]
style LOCAL fill:#22c55e,color:#fff
style SERVER fill:#3b82f6,color:#fff
style URL fill:#f59e0b,color:#fff
style REDUX fill:#8b5cf6,color:#fff
style CONTEXT fill:#ef4444,color:#fff
State Categories and Recommended Tools
| State Category | Examples | Recommended Tool |
|---|---|---|
| Local UI | Modal open/close, input fields | useState |
| Forms | Validation, field values | React Hook Form, Formik |
| Server Data | API responses, cache | RTK Query, React Query |
| URL | Page, filters, search | React Router |
| Global UI | Theme, language | Context API |
| Business Logic | Auth, cart, complex flows | Redux |
Code Splitting with combineSlices
In large applications, bundling all reducers upfront increases the initial load time. RTK 2.0's combineSlices enables lazy loading of reducers.
Basic combineSlices Usage
// app/rootReducer.js
import { combineSlices } from '@reduxjs/toolkit';
import { authSlice } from '../features/auth/authSlice';
// Start with core slices that are always needed
const rootReducer = combineSlices(authSlice);
export default rootReducer;
Lazy-Loaded Slices
// features/admin/adminSlice.js
import { createSlice } from '@reduxjs/toolkit';
import rootReducer from '../../app/rootReducer';
const adminSlice = createSlice({
name: 'admin',
initialState: { users: [], stats: null },
reducers: {
setUsers(state, action) {
state.users = action.payload;
},
},
});
// Inject this slice when the admin feature is loaded
const injectedReducer = rootReducer.inject(adminSlice);
export const { setUsers } = adminSlice.actions;
export default adminSlice.reducer;
Combining with React.lazy
// App.jsx
import React, { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// Admin page is lazy-loaded; its slice gets injected on load
const AdminPage = lazy(() => import('./pages/AdminPage'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/admin" element={<AdminPage />} />
</Routes>
</Suspense>
);
}
TypeScript version
// app/rootReducer.ts
import { combineSlices } from '@reduxjs/toolkit';
import { authSlice } from '../features/auth/authSlice';
const rootReducer = combineSlices(authSlice);
// Declare lazy-loaded slices for type safety
declare module './rootReducer' {
export interface LazyLoadedSlices {
admin: AdminState;
}
}
export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;
Error Boundary Integration
Integrate Redux error state with React error boundaries for comprehensive error handling.
// components/ReduxErrorBoundary.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
class ReduxErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasRenderError: false };
}
static getDerivedStateFromError(error) {
return { hasRenderError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Render error:', error, errorInfo);
}
render() {
// Catch render errors
if (this.state.hasRenderError) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasRenderError: false })}>
Try Again
</button>
</div>
);
}
// Catch Redux state errors
if (this.props.globalError) {
return (
<div role="alert">
<h2>Application Error</h2>
<p>{this.props.globalError}</p>
<button onClick={this.props.clearError}>
Dismiss
</button>
</div>
);
}
return this.props.children;
}
}
const mapStateToProps = (state) => ({
globalError: state.app?.globalError,
});
const mapDispatchToProps = (dispatch) => ({
clearError: () => dispatch({ type: 'app/clearGlobalError' }),
});
export default connect(mapStateToProps, mapDispatchToProps)(ReduxErrorBoundary);
Global Error Handling Middleware
// app/errorMiddleware.js
const errorMiddleware = (store) => (next) => (action) => {
// Catch all rejected async thunks
if (action.type.endsWith('/rejected')) {
const status = action.payload?.status;
if (status === 401) {
// Auto logout on unauthorized
store.dispatch({ type: 'auth/logout' });
}
if (status === 500) {
store.dispatch({
type: 'app/setGlobalError',
payload: 'A server error occurred. Please try again later.',
});
}
}
return next(action);
};
export default errorMiddleware;
10-Day Journey Recap
Let's review everything we covered over these 10 days.
| Day | Topic | Key Concepts |
|---|---|---|
| 1 | The Problem with State | Props drilling, why Redux exists |
| 2 | Three Principles of Redux | Single source of truth, immutability, pure functions |
| 3 | Redux Toolkit Basics | configureStore, createSlice, Immer |
| 4 | React-Redux Integration | useSelector, useDispatch, Provider |
| 5 | Async Operations | createAsyncThunk, loading/error state |
| 6 | RTK Query | createApi, caching, automatic refetching |
| 7 | Selectors and Performance | createSelector, memoization, re-render optimization |
| 8 | Middleware | Listener Middleware, custom middleware |
| 9 | Testing | Integration tests, MSW, renderWithProviders |
| 10 | Real-World Patterns | Auth, project structure, migration, decision guide |
Next Steps
Now that you have a solid foundation, here are resources to deepen your knowledge.
Official Documentation
- Redux Documentation -- the most authoritative resource
- Redux Toolkit Documentation -- API reference and usage guides
- RTK Query Overview -- deep dive into data fetching
Advanced Topics
- Redux Saga -- for complex async flows beyond what Listener Middleware can handle
- Normalized State -- entity management with
createEntityAdapter - Server-Side Rendering -- integrating Redux with Next.js or Remix
- State Machines -- combining XState with Redux
- Offline First -- offline support with Redux Persist
Project Ideas for Practice
| Project | Skills Practiced |
|---|---|
| Full-featured Todo App | CRUD, filtering, persistence |
| Blog Platform | RTK Query, auth, pagination |
| E-commerce Site | Cart management, checkout flow |
| Chat Application | WebSocket integration, real-time updates |
| Dashboard | Data visualization, multiple API sources |
Summary
| Pattern | Use Case | Key Tool |
|---|---|---|
| Auth Flow | Login/logout | createAsyncThunk + ProtectedRoute |
| Feature-Based Structure | Medium to large apps | features/ directory layout |
| Code Splitting | Large apps | combineSlices + React.lazy |
| Error Handling | All apps | Error Boundary + middleware |
| DevTools | Development | Redux DevTools Extension |
Final takeaways:
- Redux is a tool, not a goal -- choose the right tool for the problem
- Start small -- don't add Redux until you actually need it
- Always use RTK -- there is almost no reason to write vanilla Redux
- Write tests -- integration tests give the best return on investment
- Read the official docs -- they are the most accurate and up-to-date source
Exercises
Exercise 1: Implement an Auth Flow
Build an authentication system with:
- Email and password login
- Persisted login state (survives page reload)
- Redirect for unauthenticated users
- Logout functionality
Exercise 2: Design a Project Structure
Design the directory structure for an e-commerce app with:
- User authentication
- Product listing and search
- Shopping cart
- Order management
- User profile
Exercise 3: Choose the Right State Tool
For each of the following, pick the best state management approach (useState, Context, Redux, React Query, etc.) and explain why:
- Sidebar open/close state
- Current user information
- Product listing data
- Shopping cart contents
- Form input values
- Dark mode setting
Exercise 4: Create a Migration Plan
Write a step-by-step migration plan for converting a Todo app built with React Context and useReducer to Redux Toolkit. Ensure the app remains functional throughout the migration process.