Learn how to handle Root State in Redux Applications for effective State Management

Image generated by the author with AI and designed using Canva

As a junior developer working with Redux in a React application, one crucial concept you’ll encounter is the Root State. Properly understanding and using the Root State can significantly enhance the maintainability and scalability of your application.

In my previous tutorial Building an Authenticator App with Redux Toolkit, I explained how to build a simple authenticator app with Redux Toolkit. Taking a further step in this tutorial, I am going to explain how to use Root State to make state management better.

I’ll also discuss the pros and cons of different approaches to defining Root State. To illustrate these concepts, let’s use a scenario from one of my current projects: a farmer logs into the system and adds their lands and crop details.

What is Root State?

The Root State represents the entire state tree of your Redux store. It’s an aggregate of all the individual slices of state managed by different reducers. When using TypeScript, defining the Root State type ensures type safety and helps you avoid errors when accessing state in your components and selectors.

Why is Root State Important?

Type Safety: Ensures that your code adheres to the expected state shape, reducing runtime errors.Consistency: Provides a single source of truth for the structure of your state, making your codebase easier to understand and maintain.Predictability: Helps in maintaining a predictable state structure, which is crucial for debugging and testing.

Defining Root State

There are two common approaches to defining the Root State in TypeScript:

Explicitly Defining the State ShapeUsing ReturnType to Infer the State Shape

Scenario: A Farmer Managing Their Lands and Crops

Imagine you are developing an application where farmers can log in, manage their lands, and record crop details. Here’s how you can structure your Redux store and define the Root State for this scenario.

1. Explicitly Defining the State Shape

In this approach, you manually define the structure of your state. Here’s an example:

// types.ts

export interface Auth {
isAuthenticated: boolean;
auth: {
_id: string;
email: string;
userName: string;
role: string;
token: string;
};
}

export interface Crop {
cropName: string;
season: string;
cropType: string;
totalSoldQty: string;
totalIncome: string;
reservedQtyHome: string;
reservedQtySeed: string;
noOfPicks: string;
loanObtained: number;
userId: string;
landId: string;
_id: string;
}

export interface Land {
_id: string;
landName: string;
district: string | null;
dsDivision: string;
landRent: string;
irrigationMode: string;
userId: string;
crops: any[];
}

export interface User {
_id: string;
firstName: string;
lastName: string;
email: string;
phoneNumber: string;
nic: string;
role: string;
address: string;
}

export interface FarmerDetails {
household: string;
orgName: string;
orgAddress: string;
}

export interface OfficerDetails {
orgName: string;
orgAddress: string;
university: string;
}

// Define the structure of the Redux store’s state using the RootState type.
export type RootState = {
auth: Auth; // The state for authentication information
crop: {
crops: Crop[] | null; // An array of Crop objects, representing the state of crop data.
};
land: {
lands: Land[] | null; // An array of Land objects, representing the state of land data.
};
user: {
user: User | null; // The state for user information
};
farmer: {
farmerDetails: FarmerDetails | null; // The state for farmer details
};
officer: {
officerDetails: OfficerDetails | null; // The state for officer details
};
};

// NOTE: Use the RootState type in components and selectors to ensure type safety when accessing the Redux state.

Pros:

Clarity: The structure of your state is immediately clear.Control: You have complete control over the types.

Cons:

Manual Updates: You must manually update the RootState type if the state shape changes.Duplication: Potential duplication of type definitions.

2. Using ReturnType to Infer the State Shape

Another approach is to use TypeScript’s ReturnType utility to infer the state shape from your reducers. This ensures that the Root State is always in sync with the actual return types of the reducers.

// store.ts

import { AnyAction, configureStore, ThunkDispatch } from ‘@reduxjs/toolkit’;
import cropReducer from ‘./cropSlice’;
import landReducer from “./landSlice”;
import userReducer from “./userSlice”;
import farmerSlice from ‘./farmerSlice’;
import officerSlice from ‘./officerSlice’;
import authSlice from ‘./authSlice’;

// Create the Redux store for managing application state.
const store = configureStore({
reducer: {
auth: authSlice,
crop: cropReducer,
land: landReducer,
user: userReducer,
farmer: farmerSlice,
officer: officerSlice
},
});

// Defines a type alias for the dispatch function obtained from the Redux store
export type AppDispatch = typeof store.dispatch;

// Define the RootState type using ReturnType to infer the state shape from the reducers.
export type RootState = ReturnType<typeof store.getState>;

// Export the ‘store’ as the default export.
export default store;

Pros:

Automatic Synchronization: The RootState type is always in sync with the actual return types of the reducers.Less Maintenance: No need to manually update the RootState type when the state shape changes.

Cons:

Indirect: The shape of the state isn’t immediately visible in the RootState type itself.

Using Root State in Selectors and Components

Whether you choose to define the state shape explicitly or use ReturnType, you can use the RootState type in your selectors and components to ensure type safety.

Example: Using RootState in Selectors

// selectors.ts
import { RootState } from ‘./types’;

export const selectAuth = (state: RootState) => state.auth;
export const selectCrops = (state: RootState) => state.crop.crops;
export const selectLands = (state: RootState) => state.land.lands;
export const selectUser = (state: RootState) => state.user.user;
export const selectFarmerDetails = (state: RootState) => state.farmer.farmerDetails;
export const selectOfficerDetails = (state: RootState) => state.officer.officerDetails;

Let’s see how these concepts apply in the real-world scenario we’ve chosen.

Example: Farmer Logging In and Managing Lands and Crops

In this farmer slice, notice how the initial state is defined using the `FarmerDetails` type from `RootState` in `types.ts`. Also, observe the selector `selectFarmerDetails`, which utilizes the `RootState` type. These practices ensure the type safety of farmer data.

// Farmer Slice.ts

/**
* Represents the Redux slice responsible for managing farmer-related state and actions.
*/
import { createSlice, createAsyncThunk, PayloadAction } from ‘@reduxjs/toolkit’;
import { RootState, FarmerDetails } from ‘./types’;
import { fetchUserData } from ‘@/api/fetchUserData’;
import { UpdateFarmerData } from ‘@/api/updateFarmerData’;

// Define the initial state for the farmer slice
const initialState: { farmerDetails: FarmerDetails | null } = {
farmerDetails: null,
};

// Create an asynchronous thunk to fetch and register farmer details
export const fetchAndRegisterFarmer = createAsyncThunk(
‘farmer/fetchAndRegisterFarmer’,
async (userId: string) => {
const userData = await fetchUserData(userId);
return userData.farmerDetails;
}
);

export const updateandfetchfarmer = createAsyncThunk(
‘farmer/updateanfetchfarmer’,
async (farmerData: any) => {
const farmer = await UpdateFarmerData(farmerData);
return farmer;
}
);

// Create the farmer slice using createSlice function
const farmerSlice = createSlice({
name: ‘farmer’,
initialState,
reducers: {
// Reducer function to register farmer details
registerFarmer: (state, action: PayloadAction<FarmerDetails | null>) => {
state.farmerDetails = action.payload;
},
},
// Define extra reducers for handling asynchronous actions
extraReducers: (builder) => {
builder
// Handle successful fulfillment of fetchAndRegisterFarmer
.addCase(fetchAndRegisterFarmer.fulfilled, (state, action: PayloadAction<FarmerDetails>) => {
state.farmerDetails = action.payload;
})
// Handle rejection of fetchAndRegisterFarmer
.addCase(fetchAndRegisterFarmer.rejected, (state, action) => {
console.error(‘Error fetching farmer details:’, action.error);
});

builder
// Handle successful fulfillment of updateandfetchfarmer
.addCase(updateandfetchfarmer.fulfilled, (state, action) => {
state.farmerDetails = action.payload.farmer;
})
// Handle rejection of updateandfetchfarmer
.addCase(updateandfetchfarmer.rejected, (state, action) => {
console.error(‘Error updating farmer details:’, action.error);
});
},
});

// Export the reducer and actions from the farmer slice
export const { registerFarmer } = farmerSlice.actions;

// Selectors to retrieve farmer details from the state
export const selectFarmerDetails = (state: RootState) => state.farmer.farmerDetails;

// Export the reducer function generated by createSlice
export default farmerSlice.reducer;

Example: Using RootState in Components

In the code below, where farmer details are displayed, notice how the selectFarmerDetails selector is imported from the farmerSlice module and used to access farmerDetails. This retrieves the farmer details from the Redux store and assigns them to the farmerDetails variable.

// FarmerDashboard.tsx
import React from ‘react’;
import { useSelector, useDispatch } from ‘react-redux’;
import { RootState } from ‘./types’;
import { selectAuth, selectFarmerDetails, fetchAndRegisterFarmer } from ‘./farmerSlice’;

const FarmerDashboard: React.FC = () => {
const dispatch = useDispatch();
const auth = useSelector(selectAuth);
const farmerDetails = useSelector(selectFarmerDetails);

React.useEffect(() => {
if (auth.isAuthenticated && auth.auth._id) {
dispatch(fetchAndRegisterFarmer(auth.auth._id));
}
}, [auth, dispatch]);

return (
<div>
{auth.isAuthenticated ? (
<div>
<h1>Welcome, {auth.auth.userName}</h1>
<h2>Farmer Details:</h2>
{farmerDetails ? (
<div>
<p>Household: {farmerDetails.household}</p>
<p>Organization: {farmerDetails.orgName}</p>
<p>Address: {farmerDetails.orgAddress}</p>
</div>
) : (
<p>Loading farmer details…</p>
)}
</div>
) : (
<p>Please log in to view your dashboard</p>
)}
</div>
);
};

export default FarmerDashboard;

In our scenario, we saw how a farmer logs in, how their data is managed, and how you can leverage TypeScript to keep everything type-safe and predictable. This approach helps in building a more maintainable and scalable application, reducing the potential for runtime errors and making the development process smoother for everyone involved.

Conclusion

Using a well-defined RootState type is crucial for maintaining type safety and consistency in your Redux-based applications. Whether you choose to explicitly define the state shape or use ReturnType to infer it, ensure that your approach aligns with your team’s workflow and your project’s requirements. By doing so, you’ll help maintain a predictable and robust state management system, making it easier for your team to develop, debug, and scale the application.

For a deeper dive into managing authentication state in Redux, check out my other article, Building an Authenticator App with Redux Toolkit: A Beginner’s Guide, which will help you learn how to efficiently manage authentication state in a React application using Redux Toolkit, a powerful state management library.

If you have any questions or need clarification, please feel free to drop them in the comments below.

Happy Coding !

Understanding Redux Root State: A Guide for Junior Developers was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

​ Level Up Coding – Medium

about Infinite Loop Digital

We support businesses by identifying requirements and helping clients integrate AI seamlessly into their operations.

Gartner
Gartner Digital Workplace Summit Generative Al

GenAI sessions:

  • 4 Use Cases for Generative AI and ChatGPT in the Digital Workplace
  • How the Power of Generative AI Will Transform Knowledge Management
  • The Perils and Promises of Microsoft 365 Copilot
  • How to Be the Generative AI Champion Your CIO and Organization Need
  • How to Shift Organizational Culture Today to Embrace Generative AI Tomorrow
  • Mitigate the Risks of Generative AI by Enhancing Your Information Governance
  • Cultivate Essential Skills for Collaborating With Artificial Intelligence
  • Ask the Expert: Microsoft 365 Copilot
  • Generative AI Across Digital Workplace Markets
10 – 11 June 2024

London, U.K.