Effortless State Management with Redux Toolkit and TypeScript
Image Source: Picsum

Key Takeaways

Redux Toolkit streamlines state management by abstracting complex boilerplate and integrating seamlessly with TypeScript. By utilizing slices for modular logic and createAsyncThunk for standardized async handling, it provides a scalable architecture that ensures type safety, predictable state transitions, and a superior developer experience for modern React applications.

  • Leverage TypeScript’s ReturnType and typeof to derive RootState and AppDispatch directly from the store instance, ensuring comprehensive type safety without manual interface maintenance.
  • Adopt createSlice to colocate reducers and action creators, significantly reducing boilerplate and eliminating the risk of mismatched action types across the application.
  • Utilize createAsyncThunk to standardize asynchronous workflows, providing a structured pattern for managing pending, fulfilled, and rejected lifecycle states within extraReducers.
  • Minimize configuration overhead with configureStore, which automatically integrates Redux DevTools and middleware like redux-thunk for an optimized, production-ready development environment.

Introduction

Managing state in a web application can be a daunting task, but Redux Toolkit, in combination with TypeScript, offers an elegant solution. Redux Toolkit simplifies the setup and usage of Redux, a popular state management library. In this article, we’ll explore how to use Redux Toolkit with TypeScript, including asynchronous actions, custom actions, and type-safe store objects.

What is Redux Toolkit?

Redux Toolkit is an officially endorsed package from the Redux team that provides utilities to streamline Redux usage. It includes:

  • A simplified way to write Redux logic.
  • An ergonomic API for creating Redux stores.
  • Built-in immutability helpers.
  • Pre-configured Redux development tools for debugging.

Getting Started

Before we dive into code examples, make sure you have Redux and Redux Toolkit installed in your project, along with TypeScript.

  1. Installation:

    Begin by installing Redux Toolkit, React Redux for React integration, and TypeScript:

    npm install @reduxjs/toolkit react-redux typescript
    
  2. Setting Up the Store:

    Create a Redux store using Redux Toolkit and TypeScript. Here’s how you can set up your store:

    // store.ts
    import { configureStore } from '@reduxjs/toolkit';
    import rootReducer from './reducers'; // Your root reducer
    
    const store = configureStore({
      reducer: rootReducer,
    });
    
    export type RootState = ReturnType<typeof store.getState>;
    export type AppDispatch = typeof store.dispatch;
    
    export default store;
    

    In this code, configureStore configures the store with your root reducer. We also define types for the root state and dispatch.

Creating a Slice

Redux Toolkit encourages the use of “slices” to manage different parts of your state. Slices are a collection of reducer logic and actions that belong to a specific feature or part of your application. Let’s create a slice for a counter with async actions:

// counterSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
  loading: boolean;
  error: string | null;
}

const initialState: CounterState = {
  value: 0,
  loading: false,
  error: null,
};

export const incrementAsync = createAsyncThunk(
  'counter/incrementAsync',
  async (amount: number, thunkAPI) => {
    // Perform async operation (e.g., API call)
    return amount;
  }
);

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.loading = false;
        state.value += action.payload;
      })
      .addCase(incrementAsync.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || 'An error occurred.';
      });
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

This slice manages the state for a counter, including asynchronous increment actions.

Using Redux Toolkit in a Component

Now, let’s use Redux Toolkit in a React component with TypeScript:

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

const Counter: React.FC = () => {
  const count = useSelector((state: RootState) => state.counter.value);
  const loading = useSelector((state: RootState) => state.counter.loading);
  const error = useSelector((state: RootState) => state.counter.error);
  const dispatch: AppDispatch = useDispatch();
  const [amount, setAmount] = useState(1);

  const handleIncrement = () => {
    dispatch(increment());
  };

  const handleDecrement = () => {
    dispatch(decrement());
  };

  const handleIncrementAsync = () => {
    dispatch(incrementAsync(amount));
  };

  return (
    <div>
      <p>Count: {count}</p>
      {loading ? <p>Loading...</p> : null}
      {error ? <p>Error: {error}</p> : null}
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
      <div>
        <input type="number" value={amount} onChange={(e) => setAmount(+e.target.value)} />
        <button onClick={handleIncrementAsync}>Increment Async</button>
      </div>
    </div>
  );
};

export default Counter;

In this component, we use useSelector to access the counter state, including loading and error properties for asynchronous actions. We also use the useDispatch hook to dispatch actions.

Conclusion

Redux Toolkit simplifies state management with Redux, especially when used alongside TypeScript. It offers an ergonomic API, encourages the use of slices, and seamlessly integrates with React. By following the steps outlined in this article, you can efficiently manage state in your TypeScript-based applications using Redux Toolkit, including handling asynchronous actions. This makes your code more maintainable and scalable while ensuring type safety throughout your application.

The SQL Whisperer

The SQL Whisperer

Senior Backend Engineer with a deep passion for Ruby on Rails, high-concurrency systems, and database optimization.

Mastering Redirection in Next.js: A Complete Guide
Prev post

Mastering Redirection in Next.js: A Complete Guide

Next post

TypeScript JSON Imports: Typesafety & Intellisense

TypeScript JSON Imports: Typesafety & Intellisense