Thursday, December 26, 2024

React Project Outgrowing Expectations? Learn These Basic Principles to Manage Better

Programming LanguageReact Project Outgrowing Expectations? Learn These Basic Principles to Manage Better


Consequences of Poor Code Maintenance

Imagine building a product with great potential, but watching it progress slowly due to messy code and disorganized thinking.

We are a small team, building a product called Hexmos Feedback, Feedback helps keep teams motivated and engaged through recognition and continuous feedback.

We have attendance management as a part of Hexmos Feedback.

Hexmos Feedback goes beyond simple attendance – it helps identify committed employees who are present but may not be fully invested in the organization’s goals.

We opted for React, known for its ease of use and scalability, as the foundation for our product’s front end.

We were focused on getting features out the door, screens looking good, and functionality working. In that initial sprint, coding standards weren’t exactly a priority.

Looking back, we just threw components wherever they fit, integrated APIs directly into component files (except for that base URLs!), and cobbled together structures that barely held things together.

Again, it wasn’t the worst approach for a small team focused on a bare-bones MVP.

Growing Pains: The Need for Scalability

But then, things changed. As our capabilities matured a little bit, we started training students from various colleges, and we brought on some committed interns to help us scale our team.

That’s when the cracks began to show. Our once “functional” codebase became difficult for these newbies to navigate.

Simple bug fixes turned into hour-long hunts just to find the relevant code.

It was a wake-up call. Our codebase was hindering progress and killing developer morale. That’s when I knew we had to make a change.

Learning From Our Mistakes: Embracing Best Practices

It was time to learn some best practices, clean up the mess, and build a codebase that was scalable, maintainable, and wouldn’t make our interns tear their hair out.

Organizing for Scalability

It took me more than two hours to organize the components, fix the import errors, and things got slightly better.

I understood the importance of a well-structured codebase for easy scalability and maintenance.

Most of the code should be organized within feature folders.

Each feature folder should contain code specific to that feature, keeping things neatly separated.

Best folder structure for React

This approach helped prevent mixing feature-related code with shared components, making it simpler to manage and maintain the codebase compared to having many files in a flat folder structure.

This approach helped prevent mixing feature-related code with shared components, making it simpler to manage and maintain the codebase compared to having many files in a flat folder structure.

How Redux Simplified API Integration

Remember that initial focus on getting features out the door? While it worked for our simple MVP, things got hairy as we added more features.

Our initial approach of making API calls directly from component files became a burden.

As the project grew, managing loaders, errors, and unnecessary API calls became a major headache.

Features were getting more complicated, and our codebase was becoming a tangled mess.

Component file lines increased, business logic (API calls, data transformation) and UI logic (displaying components, handling user interactions) got tangled, and maintaining the codebase and complexity to handle loaders, errors, etc became a nightmare.

As our project matured and the challenges of managing the state grew, I knew it was time to implement a better approach.

I started using Redux for API calls, a state management library for JavaScript applications, along with thunks (like createSlice, and createAsyncThunk from @reduxjs/toolkit) to handle this problem.

This is a strong approach for complex applications as Redux acts as a central hub for your application’s state, promoting separation of concerns and scalability.

Here’s why Redux was a game-changer

1. Centralized State Management

Redux keeps all your application’s state in one central location. This makes it easier to track changes and manage loading and error states, in any component. No more hunting through a maze of component files!

2. Separation of Concerns

Thunks and slices (Redux’s way of organizing reducers and actions) allow you to cleanly separate your business logic (API calls, data transformation) from your UI logic (displaying components, handling user interactions). This makes the code more readable, maintainable, and easier to test.

3. Consistency

Redux enforces a consistent way to handle API calls and manage state. This leads to cleaner, more maintainable code across your entire project.

4. Caching for Efficiency

Redux can help with caching and memoization of data, reducing unnecessary API calls and improving performance.

We’ll dive into a concrete example in the next section to show you how Redux and thunks can be implemented to tame the API beast in your own React projects.

How to reduce burden using Redux

Here is how I got started with the conversion.
Considering you have already installed reduxjs/toolkit.

The Below image is how I have organized the files for redux in my streak feature.

Async Actions with Redux Thunk (actionCreator.ts)

First, create a file actionCreator.js and add a thunk for the API call of your component.

This file defines an asynchronous thunk action creator named fetchIndividualStreak using createAsyncThunk from @reduxjs/toolkit.

Thunks are middleware functions that allow us to perform asynchronous operations (like API calls) within Redux actions.

It handles both successful and unsuccessful responses:

  • On success, it returns the response data.
  • On failure, it rejects the promise with an error message.
// actionCreator.ts
import { createAsyncThunk } from "@reduxjs/toolkit";

export const fetchIndividualStreak = createAsyncThunk(
  "streak/fetchIndividualStreak",
  async (memberId, { rejectWithValue }) => {
    try {
      const data = {
        member_id: memberId,
      };
      const response = await apiWrapper({
        url: `${urls.FB_BACKEND}/v3/member/streak`,
        method: "POST",
        body: JSON.stringify(data),
      });
      return response;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);
Enter fullscreen mode

Exit fullscreen mode

Centralized State (reducer.ts)

This file defines a slice using createSlice from @reduxjs/toolkit.

A slice groups related reducers and an initial state for a specific feature (streak in this case).

The initial state includes properties for:

  • loading: indicates if data is being fetched
  • data: holds the fetched streak information
  • error: stores any errors encountered during the API call
// reducer.ts
import { createSlice } from "@reduxjs/toolkit";
import { fetchIndividualStreak } from "./actionCreator";

const streakSlice = createSlice({
  name: "streak",
  initialState: {
    loading: false,
    data: null,
    error: null,
  },
  reducers: ...,
  extraReducers: ...,
});

Enter fullscreen mode

Exit fullscreen mode

Continue learning about Redux State Flow During Execution here.

I’d love to hear your thoughts or suggestions for improvements!

Check out our other content

Check out other tags:

Most Popular Articles