All posts
reduxstate-managementreact

Redux: A Practical Guide for Full-Stack Developers

A practical guide to Redux — setup, core concepts, common mistakes, and production tips for full-stack developers.

SR

Suhail Roushan

April 15, 2026

·
5 min read

Redux is a predictable state container for JavaScript apps that helps you manage global application state in a consistent way.

If you've built complex frontend applications, you've likely faced the challenge of state management. Components need to share data, user actions trigger cascading updates, and debugging becomes a nightmare. Redux solves this by centralizing your application's state and logic into a single store. I've used it in production for large-scale dashboards at suhailroushan.com, and while it's not always the right tool, it's invaluable when you need it. This guide will walk through its core concepts, practical setup, and common pitfalls from a full-stack perspective.

Why Redux Matters (and When to Skip It)

Redux matters because it enforces a strict, unidirectional data flow. This makes state changes predictable and debuggable, especially in large applications with many moving parts. You can trace every UI update back to a specific action, and tools like Redux DevTools let you time-travel through state history.

However, you should skip Redux if your app is simple. For a basic form or a small widget, React's built-in useState and useContext are often sufficient. Adding Redux introduces boilerplate and complexity that isn't justified. I see developers reach for it by default, which is a mistake. Use it when you have complex state logic that needs to be shared across many components, not because it's trendy.

Getting Started with Redux

A minimal Redux setup requires a store, reducers, and actions. Here's a basic TypeScript example for a counter application.

// store.ts
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';

// Create a slice
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

// Export actions
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// Configure store
export const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
});

// Define types
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Wrap your app with the Provider and use hooks in components.

// App.tsx
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import Counter from './Counter';

function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

// Counter.tsx
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState, increment, decrement } from './store';

function Counter() {
  const count = useSelector((state: RootState) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <button onClick={() => dispatch(decrement())}>-</button>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
}

Core Redux Concepts Every Developer Should Know

Single Source of Truth: The entire application state is stored in one JavaScript object—the store. This makes it easy to debug, persist, or hydrate your app.

State is Read-Only: The only way to change state is by dispatching an action, a plain object describing what happened. This prevents direct mutations and ensures consistency.

// Action example
{ type: 'counter/increment' }
// With payload
{ type: 'counter/incrementByAmount', payload: 5 }

Changes are Made with Pure Reducers: Reducers are pure functions that take the previous state and an action, and return the next state. They must not have side effects.

// Reducer logic (simplified)
const counterReducer = (state = { value: 0 }, action) => {
  switch (action.type) {
    case 'counter/increment':
      return { ...state, value: state.value + 1 };
    case 'counter/incrementByAmount':
      return { ...state, value: state.value + action.payload };
    default:
      return state;
  }
};

Middleware for Side Effects: For asynchronous logic (like API calls), use middleware like Redux Thunk or RTK Query. Thunks allow you to write action creators that return functions.

// Async action with Redux Thunk
const fetchUserData = (userId: string) => {
  return async (dispatch: AppDispatch) => {
    dispatch({ type: 'user/fetchRequest' });
    try {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      dispatch({ type: 'user/fetchSuccess', payload: data });
    } catch (error) {
      dispatch({ type: 'user/fetchFailure', payload: error.message });
    }
  };
};

Common Redux Mistakes and How to Fix Them

1. Mutating State Directly: In a reducer, never modify the existing state object. Always return a new object.

// ❌ Wrong
state.value += 1;
return state;

// ✅ Correct (with Redux Toolkit's Immer, you can write "mutating" logic)
state.value += 1;
// Or manually:
return { ...state, value: state.value + 1 };

2. Putting Everything in Redux: Not all state belongs in the Redux store. Local UI state (like a form's temporary input value) should stay in component state.

3. Over-Normalizing Data: While normalizing state shape is good for relational data, overdoing it for simple nested objects adds unnecessary complexity. Use normalization for data that is shared and updated in multiple places.

When Should You Use Redux?

Use Redux when your application has complex state logic that needs to be shared across many components, or when you need powerful debugging capabilities like time-travel. Typical scenarios include large-scale dashboards, data-intensive applications with real-time updates, or apps where multiple users can collaborate simultaneously. If you find yourself passing props deeply through many components (prop drilling), it's a strong signal to consider Redux or a similar state management solution.

Redux in Production

In production, always use Redux Toolkit (RTK). It reduces boilerplate significantly and includes sensible defaults like Immer for safe state updates and Redux Thunk for async logic. Structure your store by features (e.g., userSlice, productsSlice) rather than by type (reducers, actions). This keeps related code together and improves maintainability. Finally, integrate RTK Query for data fetching and caching—it eliminates the need to write thunks for common API interactions and handles cache invalidation automatically.

Start your next complex feature by writing the Redux slice first—it forces you to think about state shape and actions before touching the UI.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch