Samuel Fajreldines

I am a specialist in the entire JavaScript and TypeScript ecosystem.

I am expert in AI and in creating AI integrated solutions.

I am expert in DevOps and Serverless Architecture

I am expert in PHP and its frameworks.

+55 (51) 99226-5039 samuelfajreldines@gmail.com

A Complete Guide to Redux: What It Is, How to Use It, and a Full Example

State management can become increasingly challenging as applications scale, especially in large applications built with React, Angular, or Vue. One robust solution that has emerged as a standard for state management in JavaScript and TypeScript projects is Redux. This guide will delve into the core concepts of Redux, explain how to set it up, and walk through a ready-to-run example. By the end, you’ll have a strong understanding of Redux, enabling you to manage state effectively and optimize your applications for scalability.

Why Redux Matters

When applications grow, different parts of the system often need to share and modify data. Relying on local state in components, scattered event emitters, or deeply nested props can quickly turn into a tangled mess. Redux provides a more structured approach. By centralizing application state in a single store:

• Data flow becomes more predictable.
• State transitions are easier to track.
• Debugging complex issues becomes more straightforward.
• Future changes or feature additions are simpler to implement.

These characteristics make Redux an excellent tool for scenarios that require transparency, maintainability, and robust debugging.

Core Principles of Redux

Redux is built around three core principles:

  1. Single Source of Truth:
    The entire state of your application lives in one JavaScript object known as the store. This makes it easy to inspect and debug because you have a single data structure that represents your entire state.

  2. State is Read-Only:
    The only way to change state in Redux is to dispatch an action, which is a plain object describing what happened. This prevents updates in unexpected places and keeps changes trackable.

  3. Changes are Made with Pure Functions:
    These pure functions are called reducers. They take the current state and an action, and they return the next state without mutating the original state.

When Should You Use Redux?

Redux excels in medium to large-scale applications that manage plenty of shared or frequently updated data. You might benefit from Redux if:

• Multiple components across your application require the same piece of state.
• You need a clear way to track and log state changes (such as for auditing or time-travel debugging).
• You’re building a complex application where each part of the code must remain highly predictable and testable.

For smaller or simpler applications, Redux might be overkill. However, even in small projects, having a consistent pattern for managing state can reduce bugs and improve maintainability if implemented correctly.

Getting Started with Redux

Below is a step-by-step overview of how Redux operates and how to integrate it into an application:

  1. Install Redux and Supporting Libraries
    Typically, you’ll use Redux alongside React. For instance, you would install these libraries via npm or yarn:

    npm install redux react-redux
    

    If you’re using TypeScript, you’d also install the type definitions:

    npm install --save-dev @types/redux @types/react-redux
    
  2. Create Your Actions
    Actions are plain objects that communicate facts about what happened in the application. Each action has a type field and can include additional data:

    // actions.ts
    export const INCREMENT = "INCREMENT";
    export const DECREMENT = "DECREMENT";
    
    export function increment() {
      return { type: INCREMENT };
    }
    
    export function decrement() {
      return { type: DECREMENT };
    }
  3. Define a Reducer
    The reducer is a pure function that takes the current state and an action, and returns the new state. It avoids mutating the original state, returning a newly updated state object instead:

    // reducer.ts
    import { INCREMENT, DECREMENT } from "./actions";
    
    interface CounterState {
      value: number;
    }
    
    const initialState: CounterState = {
      value: 0,
    };
    
    export function counterReducer(
      state: CounterState = initialState,
      action: { type: string }
    ): CounterState {
      switch (action.type) {
        case INCREMENT:
          return { ...state, value: state.value + 1 };
        case DECREMENT:
          return { ...state, value: state.value - 1 };
        default:
          return state;
      }
    }
  4. Create the Store
    Use Redux’s createStore function to hold your application’s state in one place. In more advanced setups, middleware and enhancers can be applied to the store:

    // store.ts
    import { createStore } from "redux";
    import { counterReducer } from "./reducer";
    
    export const store = createStore(counterReducer);
  5. Integrate with React (or Another Framework)
    If you’re using React, connect your components using react-redux. The Provider component makes the store available to any nested components:

    // index.tsx
    import React from "react";
    import ReactDOM from "react-dom";
    import { Provider } from "react-redux";
    import { store } from "./store";
    import App from "./App";
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById("root")
    );

A Complete Example

The following is a fully functioning example using TypeScript. This walkthrough spans from creating Redux actions and reducers to connecting everything in a React application. This example illustrates key best practices to keep in mind for scalability and testability.

Step 1: Project Structure

Organizing files effectively is crucial for large codebases. A possible layout might look like this:

src
├── components
│   └── Counter.tsx
├── redux
│   ├── actions.ts
│   ├── reducer.ts
│   └── store.ts
└── App.tsx

Keep in mind that the structure can be adapted based on the application’s size and complexity.

Step 2: Create Action Types and Action Creators

// src/redux/actions.ts
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

interface IncrementAction {
  type: typeof INCREMENT;
}

interface DecrementAction {
  type: typeof DECREMENT;
}

export type CounterActionTypes = IncrementAction | DecrementAction;

export function increment(): CounterActionTypes {
  return { type: INCREMENT };
}

export function decrement(): CounterActionTypes {
  return { type: DECREMENT };
}

Here, we are using TypeScript interfaces for each action type, ensuring our code remains type-safe. The increment and decrement functions are classic examples of action creators.

Step 3: Define the Reducer

// src/redux/reducer.ts
import { INCREMENT, DECREMENT, CounterActionTypes } from "./actions";

export interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export function counterReducer(
  state = initialState,
  action: CounterActionTypes
): CounterState {
  switch (action.type) {
    case INCREMENT:
      return { ...state, value: state.value + 1 };
    case DECREMENT:
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}

The counterReducer function demonstrates how Redux enforces a single source of truth. Only this function can modify state, and it does so by returning a new state object rather than mutating the current one.

Step 4: Create the Store

// src/redux/store.ts
import { createStore } from "redux";
import { counterReducer } from "./reducer";

export const store = createStore(counterReducer);

This code spins up a simple Redux store. For more complex applications, applyMiddleware can be used to add middleware such as Redux Thunk or Redux Saga for handling side effects.

Step 5: Build a Counter Component

// src/components/Counter.tsx
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "../redux/actions";
import { CounterState } from "../redux/reducer";

export function Counter() {
  const dispatch = useDispatch();
  const value = useSelector((state: CounterState) => state.value);

  return (
    <div>
      <h1>Redux Counter</h1>
      <p>Value: {value}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
}

In this component, the useSelector hook retrieves the current counter value from the Redux store, while the useDispatch hook allows for dispatching actions.

Step 6: Wire Everything Up in the App

// src/App.tsx
import React from "react";
import { Counter } from "./components/Counter";

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

export default App;

Finally, we have a simple App component that renders our Counter.

Running the Application

With the above structure in place, running the application is typically as simple as:

  1. Installing dependencies:
    npm install
    
  2. Starting the development server:
    npm start
    

Open a browser to view the application, and you’ll see the counter that can be incremented or decremented while preserving the central state in Redux.

Best Practices for Scaling Redux

  1. Use Middleware for Side Effects:
    When the application requires handling asynchronous operations (e.g., fetching data from an API), add middleware like redux-thunk or redux-saga. These libraries provide structured patterns for dealing with side effects.

  2. Normalize State for Complex Entities:
    If your application deals with arrays of items or deeply nested data, it’s often more efficient to store data in normalized form. Libraries such as normalizr can help flatten complex data structures for easier updates and retrieval.

  3. Keep Reducers Focused:
    Each reducer should handle a specific piece of the application’s state. This fosters modularity and makes it simpler to maintain or replace features later on.

  4. Combine Reducers:
    For larger projects, use combineReducers to split logic across multiple reducers. Each reducer manages its slice of the state, so you end up with something more maintainable and testable.

  5. Embrace the DevTools:
    The Redux DevTools Extension offers time-travel debugging and powerful insights into state changes. This can significantly speed up development and debugging in production-like environments.

  6. Use TypeScript Effectively:
    Properly typed actions, reducers, and selectors reduce the likelihood of runtime errors. Clear types and interfaces make reasoning about state transitions more intuitive, significantly aiding in large-scale projects.

Common Pitfalls and How to Avoid Them

  1. Overusing Redux:
    Not all state belongs in Redux. Sometimes, local component state is simpler for UI-specific flags. Keep Redux for data that needs to be shared widely or is critical to the application’s integrity.

  2. Mutating State in Reducers:
    Directly mutating the state can lead to subtle bugs that are difficult to diagnose. Always return a new copy of the state object:

    • Use object or array spread operators.
    • Avoid direct reassignments (e.g., state.value++).

  3. Ignoring Performance:
    In large-scale applications, excessive re-renders can degrade performance. Techniques like memoization or reselect-based selectors help compute derived data efficiently and prevent irrelevant re-renders.

  4. Neglecting Code Organization:
    As a codebase grows, improper organization can lead to confusion. Keep actions, reducers, and types clearly separated. This clarity helps when multiple teams or developers collaborate.

Beyond the Basics

After setting up a solid foundation, you can explore advanced Redux patterns and ecosystem tools:

• Redux Observable: Manage complex async flows using RxJS.
• Reselect: Create composable, memoized selectors for performance improvements.
• Redux Toolkit: Offers opinionated approaches to reduce boilerplate and promote good patterns.
• TypeScript Utility Types: Leverage utility types like ReturnType, Extract, and more to simplify type definitions in advanced scenarios.

Final Thoughts on Redux

Redux remains a heavyweight champion for predictable state management in large-scale JavaScript and TypeScript applications. By following its core principles—single source of truth, read-only state, and pure reducers—developers can build maintainable, testable, and scalable applications. While Redux might seem verbose initially, especially with the additional layer of TS types, the rigorous structure it imposes often proves invaluable as an application’s complexity grows.

Whether building a new project from scratch or refactoring an existing codebase, Redux provides a solid backbone for data flow. It integrates seamlessly with various UI and server-side architectures, making it a versatile choice in the broader JavaScript and TypeScript ecosystem. By adhering to best practices and leveraging the powerful developer tools available, Redux can become a key piece of any high-performing, future-proof application.


Resume

Experience

  • SecurityScoreCard

    Nov. 2023 - Present

    New York, United States

    Senior Software Engineer

    I joined SecurityScorecard, a leading organization with over 400 employees, as a Senior Full Stack Software Engineer. My role spans across developing new systems, maintaining and refactoring legacy solutions, and ensuring they meet the company's high standards of performance, scalability, and reliability.

    I work across the entire stack, contributing to both frontend and backend development while also collaborating directly on infrastructure-related tasks, leveraging cloud computing technologies to optimize and scale our systems. This broad scope of responsibilities allows me to ensure seamless integration between user-facing applications and underlying systems architecture.

    Additionally, I collaborate closely with diverse teams across the organization, aligning technical implementation with strategic business objectives. Through my work, I aim to deliver innovative and robust solutions that enhance SecurityScorecard's offerings and support its mission to provide world-class cybersecurity insights.

    Technologies Used:

    Node.js Terraform React Typescript AWS Playwright and Cypress