Getting Started with Redux + TypeScript

Hello everyone! In this article, we’ll explore how to set up Redux with TypeScript in a Next.Js app. Additionally, we'll dive into implementing redux-thunk for handling asynchronous actions in another blog to keep it short. We’ll reference the official documentation throughout to guide us.

Prerequisites:
Before we begin, I assume you have a basic understanding of React's Context API or state management techniques. You should also be familiar with TypeScript and have experience building a React or Next.js app.

Let's get started!

Create a Next.js App

npx create-next-app@latest

Here is what it looks.

After successfully setting up the Next.js app, navigate into your project folder & run npm run dev.

Now that the setup is complete, let’s remove the default content from the Next.js template. I won’t go into details about Next.js itself, as this is not a Next.js tutorial. Instead, we'll focus on integrating Redux with TypeScript.

Setting Up Redux

  1. Installation

    To begin, we need to install two essential packages: react-redux and @reduxjs/toolkit.

    Run the following command in your terminal where you have run the npm run dev command pervious or create another terminal with similar path:

     npm install react-redux
     npm install @reduxjs/toolkit
    
     # Or if you use Yarn:
    
     # yarn add react-redux
     # yarn add @reduxjs/toolkit
    
  2. Setting Up the Global Store

    Now, let’s create the global store that will hold all the states used in our application. We’ll set this up in the src/store/store.ts file.

    Create the store.ts file and add the following code:

     // src/store/store.ts
     import { configureStore } from '@reduxjs/toolkit';
    
     export const store = configureStore({
       reducer: {
         // We will link all the reducers (states) here
       }
     });
    
     // Infer the `RootState`, `AppDispatch`, and `AppStore` types from the store
     export type RootState = ReturnType<typeof store.getState>;
     export type AppDispatch = typeof store.dispatch;
     export type AppStore = typeof store;
    

    In this file:

    • We’re using configureStore from @reduxjs/toolkit to set up our store.

    • RootState is inferred from the store’s state, giving us type safety throughout our app.

    • AppDispatch and AppStore are also typed based on the store itself, which helps TypeScript infer the correct types for dispatch and store access.

We'll link specific reducers later as we define our slices of state.

  1. Creating a Custom Hook for Typed Redux

    Next, we’ll create a hook.ts file that will provide easy TypeScript support when using useSelector and useDispatch. Without this file, we would need to manually define the types for useSelector and useDispatch each time we use them. By setting this up, TypeScript will automatically know the types of the state and dispatched actions, making our development smoother.

    Create the src/store/hook.ts file and add the following code:

     // src/store/hook.ts
     import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
     import type { RootState, AppDispatch } from './store';
    
     // Use throughout your app instead of plain `useDispatch` and `useSelector`
     export const useAppDispatch: () => AppDispatch = useDispatch;
     export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
    

    In this file:

    • We’re creating custom hooks useAppDispatch and useAppSelector.

    • useAppDispatch will be typed based on our AppDispatch from the store, so it knows the correct actions and their types.

    • useAppSelector is typed with RootState, meaning it will automatically suggest the available state values and their types when accessing the store.

By using these hooks, we avoid repetitive typing, making our code cleaner and more maintainable.

  1. Defining State with Slices

    Now, let's define the state that will be used across the application. To keep things organized, we’ll categorize related states into groups. For instance, user-related information such as name, email, and website will be stored in a single category called userStates. These groups of related states are commonly referred to as slices, hence the term userSlice.

    We’ll create a folder inside store named slices to manage these categories.

    Create userSlice.ts

    First, create a userSlice.ts file in src/store/slices/user and add the following code:

     // src/store/slicces/user/userSlice.ts
    
     import { createSlice } from '@reduxjs/toolkit'
    
     // Define a type for the slice state
     export interface IUserStates {
         name: string;
         email: string;
         website: string;
     }
    
     // Define the initial state using that type
     const initialState: IUserStates = {
         name: 'Satya Dalei',
         email: 'satyaprofessional99@gmail.com',
         website: 'https://satyadalei.vercel.app'
     }
    
     export const userSlice = createSlice({
         name: 'user',
         // `createSlice` will infer the state type from the `initialState` argument
         initialState,
         reducers: {
             // Define reducers for each action
             setName: (state, action) => {
                 state.name = action.payload
             },
             setEmail: (state, action) => {
                 state.email = action.payload
             },
             setWebsite: (state, action) => {
                 state.website = action.payload
             },
             resetUser: (state, action) => {
                 if (action.payload) {
                     state = action.payload
                 }else {
                     state = initialState
                 }
             }
         }
     })
    
     export const { resetUser, setEmail, setName, setWebsite } = userSlice.actions;
     export default userSlice.reducer
    

    Explanation:

    • Slice State: We define a TypeScript interface IUserStates that describes the shape of the user state (name, email, and website).

    • Initial State: The initialState is where we define the default values for the user.

    • Reducers: We define several reducers, such as setName, setEmail, and setWebsite, which are responsible for updating individual pieces of state. The resetUser reducer resets the state back to either the initial values or a provided payload.

    • Exported Actions and Reducer: The createSlice function automatically generates the necessary actions and reducers based on our configuration, which we can use throughout our app.

By using createSlice, Redux Toolkit automatically infers the types from the initialState, making the code more concise and type-safe.

  1. Linking State to the Global Store

    Now that we’ve defined our userSlice, the next step is to integrate it into the global store. We’ll add the userSlice reducer to our store.ts so that the user state becomes part of the overall application state.

    Here’s how you can link it:

     /*
      src/store/store.ts
      This file acts as the central store for all the states in our application.
     */
     import { configureStore } from '@reduxjs/toolkit';
     import userReducer from '@/store/slices/user/userSlice';
    
     export const store = configureStore({
       reducer: {
         user: userReducer,  // Linking userSlice to the store
       },
     });
    
     // Infer the `RootState`, `AppDispatch`, and `AppStore` types from the store
     export type RootState = ReturnType<typeof store.getState>;
     export type AppDispatch = typeof store.dispatch;
     export type AppStore = typeof store;
    

    Explanation:

    • Global Store: We’re using configureStore to register the userReducer (from userSlice) under the key user. This means that all user-related state will be accessible via state.user.

    • TypeScript Inference: We continue to infer types like RootState and AppDispatch from the store configuration, ensuring strong type safety across the app.

At this point, your userSlice is now part of the global Redux store, making the user state available throughout your application.

  1. Setting Up the Redux Provider

    To make the Redux store accessible to all components in your app, you need to set up the Provider from react-redux.

    We'll create a ReduxProvider component that will handle this setup.

    Create ReduxProvider.tsx

    Create a file at src/providers/ReduxProvider.tsx with the following code:

     /*
     src/providers/ReduxProvider.tsx
     This file provides the global store to our entire app.
     */
     "use client";  // Ensure this file runs on the client side
    
     import { store } from '@/store/store';
     import React from 'react';
     import { Provider } from 'react-redux';
    
     interface IReduxProviderProps {
         children: React.ReactNode;
     }
    
     const ReduxProvider: React.FC<IReduxProviderProps> = ({ children }) => {
       return (
         <Provider store={store}>
           {children}
         </Provider>
       );
     };
    
     export default ReduxProvider;
    

    Explanation:

    • ReduxProvider Component: This component wraps the Provider from react-redux around any children components, passing the store to make the state globally accessible.

    • Props: We define IReduxProviderProps to ensure the component can accept children, which will be the rest of your app.

  2. Wrapping the Redux Provider in the App Layout

    With the ReduxProvider set up, we need to wrap our application to ensure the Redux store is available globally.

    Update layout.tsx as follows:

     /*
     src/app/layout.tsx
     Wraps the entire application with ReduxProvider to provide store access.
     */
     import type { Metadata } from "next";
     import { Inter } from "next/font/google";
     import "./globals.css";
     import ReduxProvider from "@/providers/ReduxProvider";
    
     const inter = Inter({ subsets: ["latin"] });
    
     export const metadata: Metadata = {
       title: "Create Next App",
       description: "Generated by create next app",
     };
    
     export default function RootLayout({
       children,
     }: Readonly<{
       children: React.ReactNode;
     }>) {
       return (
         <html lang="en">
           <body className={inter.className}>
             <ReduxProvider>
               {children}
             </ReduxProvider>
           </body>
         </html>
       );
     }
    

    Explanation:

    • Integration: By wrapping the children with ReduxProvider, the Redux store is now available to all components in your Next.js app.

    • Client-Side Rendering: Ensure the ReduxProvider is used correctly with client-side rendering for compatibility with Next.js.

  3. Why Use a Separate ReduxProvider.tsx?

    You might wonder why we create a separate ReduxProvider.tsx instead of directly wrapping the Provider in layout.tsx. Let's explore why this approach is necessary, especially when working with Next.js.

    Why Not Directly in layout.tsx?

    You might think about directly including the Provider in layout.tsx like this:

     /*
     src/app/layout.tsx
     */
     import type { Metadata } from "next";
     import { Inter } from "next/font/google";
     import "./globals.css";
     import { Provider } from "react-redux";
     import { store } from "@/store/store";
    
     const inter = Inter({ subsets: ["latin"] });
    
     export const metadata: Metadata = {
       title: "Create Next App",
       description: "Generated by create next app",
     };
    
     export default function RootLayout({
       children,
     }: Readonly<{
       children: React.ReactNode;
     }>) {
       return (
         <html lang="en">
           <body className={inter.className}>
             <Provider store={store}>
               {children}
             </Provider>
           </body>
         </html>
       );
     }
    

    The Issue

    1. Server-Side vs. Client-Side Rendering:

      • Next.js runs both on the server and client sides. The Provider from react-redux relies on client-side rendering since it handles state management in the browser.

      • Adding the Provider directly in layout.tsx without “use client” will cause errors because Next.js expects server-side rendering for pages.

    2. Metadata and "use client" Directive:

      • Adding "use client" at the top of layout.tsx makes it a client-side component, but it also conflicts with the export of metadata, which is meant for server-side processing.

      • This conflict results in errors and makes it impossible to use Provider directly in the layout.tsx file.

The Solution

To resolve these issues, we use a dedicated ReduxProvider.tsx file as in the above.

  1. Accessing State

    Now that we've set up Redux and the Provider, it’s time to access the state in your components. To do this, we'll use the useAppSelector hook that we created earlier.

    Accessing State in a Component

    Here's how you can access and display the state in a component:

     /*
     src/app/page.tsx
     Make sure to include "use client" at the top because we are using useAppSelector,
     which only works on the client side.
     */
     "use client";
    
     import { useAppSelector } from "@/store/hook";
    
     export default function Home() {
       // Access user state using useAppSelector
       const { email, name, website } = useAppSelector((state) => state.user);
    
       return (
         <div>
           <h1>Hello</h1>
           <p>Name: {name}</p>
           <p>Email: {email}</p>
           <p>Website: {website}</p>
         </div>
       );
     }
    

  1. Modifying State

    Now that we can access the state, let’s see how to update it. We’ll use the Redux dispatch function to call the appropriate reducer function and modify the state.

    Updating State in a Component

    Here’s how you can update the state using a form element and a dispatch action:

/*
src/app/page.tsx
Here we are creating a input element & we are resetting it to the 
global store using dispatch function
*/
"use client"
import { useAppDispatch, useAppSelector } from "@/store/hook";
import { setName } from "@/store/slices/user/userSlice";
import { ChangeEvent, useState } from "react";

export default function Home() {
  const { email, name, website } = useAppSelector((state) => state.user);
  const dispatch = useAppDispatch();

  const [userName, setUserName] = useState<string>("");

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setUserName(e.target.value);
  }

  const setNameInRedux = ()=>{
    dispatch(setName(userName));
  }
  return (
    <div>
      <h1>Hello</h1>
      <p>Name {name}</p>
      <p>Email {email}</p>
      <p>website {website}</p>

      <div className="mt-5" >
        <input className="border-2 border-black-600" value={userName} onChange={handleChange} type="text" />
        <button onClick={setNameInRedux} >
          change Name
        </button>
      </div>
    </div>
  );
}

Explanation:

  • useAppDispatch Hook: This hook provides the dispatch function to dispatch actions to the Redux store.

  • useState Hook: Local state userName is used to manage the input field value.

  • handleChange Function: Updates the local state with the current value of the input field.

  • setNameInRedux Function: Dispatches the setName action to update the name in the Redux store with the current value of userName.

  • Input and Button: The input field captures user input, and the button triggers the setNameInRedux function to update the Redux state.

Conclusion

With this setup, you can both access and update the state using Redux in your Next.js app. The combination of useAppSelector and useAppDispatch hooks allows you to efficiently interact with the Redux store.

We will explore how to use redux store with asynchronously or in the api call. Stay tuned for that. Thank you for reading.

0
Subscribe to my newsletter

Read articles from Satyanarayan Dalei directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Satyanarayan Dalei
Satyanarayan Dalei

Hi, I'm Satyanarayan Dalei, a mid-level Full-stack web developer from India. Currently pursuing a master's in Computer Application, I've been coding since 2020. My expertise lies in the MERN stack, and I am well-versed in the software deployment life cycle, covering both production and development environments.