How to Architect a React Application Using Feature-Based Architecture

How to Architect a React Application Using Feature-Based Architecture

What is Architecture? Architecture is about making decisions early that are difficult to change later. In software, it defines the foundation upon which your application will evolve.

Software Architecture Software architecture refers to the high-level structure of a software system. It defines the components, their relationships, interactions, and how the entire system works.

Many applications are developed without a proper architecture or design pattern. Often, businesses only consider refactoring when scaling becomes necessary. The better approach is to plan and align architecture from the very beginning.

In this article, we’ll explore how to architect a React application effectively.


Step 1: Create an Architecture Decision Record (ADR)

The first step is to document your architectural decisions using an ADR. This ensures clarity and consistency across the project. If you need an example, refer to the attached document.


Step 2: Choose Feature-Based Architecture

For modern applications, a Feature-Based Architecture aligns well with SOLID principles.

Feature-based architecture organizes the codebase around business features or domains rather than technical layers.

Example Folder Structure:

- features
    - auth
      - pages
      - providers
      - schemas
      - services
      - components
      - hooks
      - stores
    - project
    - product
- constants
- hooks
- providers
- services
- types
- utils
- layouts
- shared
    - components 
    - libs
        

Key Concept: Each feature is isolated. Removing a feature like project should not break the product feature at the code level. UI routes might break, but internally the system remains stable.


Step 3: Apply SOLID Principles

During development, follow SOLID design principles, which naturally align with feature-based architecture.`


Step 4: App State Management with Provider + Facade/Adapter Pattern


Article content


State management is crucial for production-level architecture. We recommend using a Provider with Facade + Adapter pattern.

Example App Component Setup:

<Suspense>
  <QueryProvider>
    <AuthProvider>
      <RouterProvider router={routes} />
    </AuthProvider>
  </QueryProvider>
</Suspense>
        

Router Example with Role Handling:

 
  element: <DashboardLayout />,
  children: [
    {
      path: "/dashboard",
      element: <Dashboard />,
      handle: {
        roles: [Roles.ADMIN], // From root constants
      },
    },
  ],
 
        

During initial page load, AuthProvider drives the application logic.


Step 5: Understanding Facade + Adapter Pattern for State

Think of it like a TV remote:

  • Pressing the remote does not require knowing how the TV works internally.
  • Similarly, components in your application do not need to know the inner workings of the state manager (Redux, Zustand, TanStack, etc.).

This abstraction simplifies code and reduces coupling.


Example: AuthProvider

import { useStore } from "@tanstack/react-store";
import { AuthContext, AuthBootstrap } from "@/features/auth/providers";
import { AuthStore } from "@/features/auth/stores";

export function AuthProvider({ children }: PropsWithChildren) {
  const [isInitializing, setIsInitializing] = useState(true);

  const AuthContextValue = {
    isAuthenticated: useStore(AuthStore.isAuthenticated),
    setIsAuthenticated: AuthStore.setIsAuthenticated,
    authToken: useStore(AuthStore.authToken),
    setRole: AuthStore.setRole,
    role: useStore(AuthStore.role),
    profile: useStore(AuthStore.profile),
    setProfile: AuthStore.setProfile,
    isLoading: isInitializing,
  };

  return (
    <AuthContext.Provider value={AuthContextValue}>
      <

 onInitialized={() => setIsInitializing(false)} />
      {children}
    </AuthContext.Provider>
  );
}
        

AuthContext

import { createContext, useContext } from "react";
import { UserProfile } from "@/features/auth/schemas/dtos";
import { Role } from "@/constants/auth";

export type AuthContextValue = {
  isAuthenticated?: boolean;
  authToken?: string;
  profile?: UserProfile;
  isLoading: boolean;
  role?: Role;

  setIsAuthenticated(state: boolean): void;
  setAuthToken(token: string | undefined): void;
  setProfile(profile: UserProfile): void;
  setRole(roleValue: Role | undefined): void;
};

export const AuthContext = createContext<AuthContextValue | null>(null);

export function useAuthContext() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuthContext must be used within a provider");
  return ctx;
}
        

Usage in a Component:

const { setAuthToken, setProfile, setIsAuthenticated, setRole } = useAuthContext();
        

The component does not need to know what happens inside the state manager — it only consumes the abstraction.


AuthBootstrap: Token Validation

We do a token validation against back-end for user website revisits.

import { useEffect } from "react";
import { AuthStore, getAuthToken } from "@/features/auth/stores";
import { useValidateToken } from "@/features/auth/hooks";

export function AuthBootstrap({ onInitialized }: { onInitialized: () => void }) {
  const validateTokenMutation = useValidateToken();

  useEffect(() => {
    const token = getAuthToken();
    if (!token) {
      onInitialized();
      return;
    }

    validateTokenMutation.mutate(token, {
      onSettled: () => onInitialized(),
      onError: () => AuthStore.setIsAuthenticated(false),
    });
  }, []);

  return null;
}
        

Token Validation Hook:

import { useMutation } from "@tanstack/react-query";
import { authService } from "@/features/auth/services/register";
import { ApiError, ResponseDto } from "@/features/auth/schemas/dtos";
import { useAuthContext } from "@/features/auth/providers";

export function useValidateToken() {
  const { setAuthToken, setProfile, setIsAuthenticated, setRole } = useAuthContext();

  return useMutation<ResponseDto, ApiError, string>({
    mutationFn: async (token: string) => authService.validateToken(token),
    onSuccess: (data, token) => {
      if (data?.role) {
        setAuthToken(token);
        setProfile(data.profile);
        setIsAuthenticated(true);
        setRole(data.role);
      }
    },
  });
}
        

Conclusion

By combining Feature-Based Architecture with SOLID principles and Facade + Adapter state management, you can build scalable, maintainable, and modular React applications. This approach ensures features remain isolated, code is easy to maintain, and your application is ready for production growth.

To view or add a comment, sign in

More articles by Piyal Darshana

Explore content categories