Simplifying Workflows, Maximizing Impact

Call at: 07850 074505

Scaling with React

Whether we build a system from scratch or take an existing framework and scale it to deliver features, improve performance, and serve a larger audience, we must ensure our code is structured properly.

Of course, we could argue that a clean codebase is always important. However, it becomes critical when dealing with scalability challenges.

In this article, I will focus on state management in React and share my experience with two different approaches.

Harnessing the Power of State in React

State management is a fundamental concept in React. At its simplest, we can add state to a React component using useState:

export const MyComponent: React.FC = () => { const [valid, setValid] = useState(false); return ( <span style={{ color: valid ? "red" : "black" }}>Status</span> ); };


To make this interactive, we can add a button that toggles the state when clicked:

<button onClick={() => setValid((prev) => !prev)}>Toggle Status</button>


This basic example is well-documented across the internet. However, when working on large-scale applications, more advanced state management strategies become necessary.

Moving Away from Redux

One popular approach to managing state globally is Redux. I used Redux extensively, but after several months, I decided to move away from it due to the excessive boilerplate. While Redux is powerful, it can increase complexity and reduce maintainability compared to other solutions.

Embracing Local Context for State Management

Thanks to insights from Wes Bos and other React experts, I discovered an alternative approach—using Local Context for state management.

I decided to refactor my code and migrate all my state to use Local Context.

This approach worked exceptionally well, allowing me to successfully deliver five different systems with varied complexity and scale. These applications all follow the same architecture, where:

This consistency made the applications easier to maintain and scale.

When Local Context Isn’t Enough: A New Challenge

While Local Context worked well for most use cases, I encountered a scenario that required a different approach—I needed to update state while a component was loading.

Typically, we use useEffect for this, and it often works well. However, as my codebase grew, I noticed that performance issues began to surface:

With the help of ChatGPT, I discovered an alternative that not only resolved my issue but also improved my approach to state management.

Introducing Scoped State: A More Flexible Approach

In my latest projects, 90% of my state remains global—and it works well. However, 10% of my state now follows a Scoped State approach.
What is Scoped State?
Scoped State follows the same structure as my previous Local Context approach but with one key difference:

Pros & Cons of Scoped State

Pros:
Allows initialisation parameters to be passed dynamically
Helps prevent unnecessary re-renders
Keeps state logic modular and localized
Cons:
Requires adding the state provider to each component that needs it
Despite the additional setup required for each component, this approach has proven to be a powerful tool for optimising performance in React applications.

Final Optimised State Code: Troubleshooting with ChatGPT

The optimisations ChatGPT introduced in my state management code included integrating useRef and useCallback. These additions prevented infinite re-renders in React and improved performance.

By using useRef, I ensured that my state initialisation only happened once, avoiding unnecessary updates. Meanwhile, useCallback helped stabilise functions, preventing re-renders triggered by function recreations.

With these refinements, my Scoped State approach remains as structured as before but now operates more efficiently. This solution has resolved my issue while keeping my React components scalable and maintainable.

import { createContext, ReactNode, useContext, useEffect, useCallback, useRef } from "react";
import { useImmer } from "use-immer";

interface EventInfoState {
    activeEventId: string | undefined;
    shampoo: boolean;
}

interface EventState {
    eventState: EventInfoState;
    resetActiveEvent: () => void;
    toggleActiveEvent: (id: string) => void;
    toggleShampooEvent: () => void;
}

const initialState: EventInfoState = {
    activeEventId: '',
    shampoo: false,
};

const LocalStateContext = createContext<EventState | undefined>(undefined);

interface EventStateProviderProps {
    children: ReactNode;
    eventGroup?: { eventHosts: { eventId: string }[] };
}

const EventStateProvider: React.FC<EventStateProviderProps> = ({ children, eventGroup }) => {
    const [state, setState] = useImmer<EventInfoState>(initialState);
    const initialized = useRef(false); // Prevent redundant state updates

    const toggleActiveEvent = useCallback((id: string) => {
        setState(draft => {
            draft.activeEventId = id;
        });
    }, [setState]);

    const resetActiveEvent = useCallback(() => {
        setState(draft => {
            draft.activeEventId = undefined;
        });
    }, [setState]);

    const toggleShampooEvent = useCallback(() => {
        setState(draft => {
            draft.shampoo = !draft.shampoo;
        });
    }, [setState]);

    // Initialize activeEventId if only one eventHost exists
    useEffect(() => {
        if (initialized.current) return; // Prevent multiple initializations

        if (eventGroup?.eventHosts?.length === 1 && !state.activeEventId) {
            setState(draft => {
                draft.activeEventId = eventGroup.eventHosts[0].eventId;
            });
            initialized.current = true; // Mark as initialized
        }
    }, [eventGroup?.eventHosts, setState]);

    return (
        <LocalStateContext.Provider
            value={{
                resetActiveEvent,
                toggleActiveEvent,
                toggleShampooEvent,
                eventState: state
            }}
        >
            {children}
        </LocalStateContext.Provider>
    );
};

function useEventState(): EventState {
    const context = useContext(LocalStateContext);
    if (!context) {
        throw new Error("useEventState must be used within an EventStateProvider");
    }
    return context;
}

export { EventStateProvider, useEventState };