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.