Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions docs/issue-42-[section]-scalable-application.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Scalable Application Structure

This section outlines a strategy for organizing a React + Redux + TypeScript application to ensure long-term maintainability, scalability, and reusability. The primary goal is to move away from grouping files by type (e.g., all actions in one folder, all reducers in another) and instead group them by **feature**.

## Core Goals

1. **Feature-Based Grouping**: All logic related to a specific domain (e.g., `User`, `Product`, `Checkout`) lives together.
2. **Easy Addition/Removal**: Features can be added or removed by simply adding or deleting a folder, with minimal impact on the rest of the codebase.
3. **On-Demand Enable/Disable**: Features can be conditionally loaded or disabled via configuration without breaking the application.
4. **Reusability & Pluggability**: Features are self-contained modules that can be dropped into different Redux applications with minimal wiring.

## Recommended Directory Structure

The following structure demonstrates a scalable architecture where features are isolated.

```text
src/
├── app/ # App-level configuration, entry point, global styles
│ ├── index.tsx
│ ├── store.ts # Root store configuration
│ └── App.tsx
├── features/ # Feature modules (The core of scalability)
│ ├── auth/ # Feature: Authentication
│ │ ├── components/ # UI components specific to Auth
│ │ ├── hooks/ # Custom hooks for Auth logic
│ │ ├── slices/ # Redux Slice (Actions, Reducer, Selectors)
│ │ │ └── authSlice.ts
│ │ ├── services/ # API calls specific to Auth
│ │ └── index.ts # Public API (exports only what is needed)
│ ├── products/ # Feature: Product Management
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── slices/
│ │ │ └── productsSlice.ts
│ │ ├── services/
│ │ └── index.ts
│ └── cart/ # Feature: Shopping Cart
│ ├── components/
│ ├── hooks/
│ ├── slices/
│ │ └── cartSlice.ts
│ ├── services/
│ └── index.ts
├── shared/ # Shared resources used across multiple features
│ ├── components/ # Generic UI components (Button, Modal, Input)
│ ├── hooks/ # Generic hooks (useDebounce, useLocalStorage)
│ ├── types/ # Shared TypeScript interfaces
│ └── utils/ # Utility functions
└── config/ # Environment variables and feature flags
├── index.ts
└── featureFlags.ts
```

## Implementation Details

### 1. Feature Isolation via Slices

Each feature should define its own Redux Slice using `@reduxjs/toolkit`. This encapsulates state, actions, and reducers within the feature folder.

**Example: `src/features/auth/slices/authSlice.ts`**

```typescript
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface AuthState {
user: { id: string; name: string } | null;
isAuthenticated: boolean;
isLoading: boolean;
}

const initialState: AuthState = {
user: null,
isAuthenticated: false,
isLoading: false,
};

const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
loginStart: (state) => {
state.isLoading = true;
},
loginSuccess: (state, action: PayloadAction<{ id: string; name: string }>) => {
state.isLoading = false;
state.isAuthenticated = true;
state.user = action.payload;
},
loginFailure: (state) => {
state.isLoading = false;
state.isAuthenticated = false;
},
logout: (state) => {
state.user = null;
state.isAuthenticated = false;
},
},
});

export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions;
export default authSlice.reducer;
Comment on lines +100 to +101

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The public API example (index.ts) in the next section attempts to export selectUser and selectIsAuthenticated from ./slices/authSlice. However, these selectors are not defined in authSlice.ts. Defining them here makes the example complete and functional.

Suggested change
export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions;
export default authSlice.reducer;
export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions;
export const selectUser = (state: { auth: AuthState }) => state.auth.user;
export const selectIsAuthenticated = (state: { auth: AuthState }) => state.auth.isAuthenticated;
export default authSlice.reducer;

```

### 2. The Public API Pattern (`index.ts`)

To ensure features are pluggable and reusable, each feature folder should export a clean public API via its `index.ts` file. This prevents other parts of the app from importing internal implementation details (like specific component files or private utilities).

**Example: `src/features/auth/index.ts`**

```typescript
// Export the reducer for the store configuration
export { default as authReducer } from './slices/authSlice';

// Export actions
export { loginStart, loginSuccess, loginFailure, logout } from './slices/authSlice';

// Export selectors (if defined in the slice or a separate file)
export { selectUser, selectIsAuthenticated } from './slices/authSlice';

// Export components (only those meant for public use)
export { LoginForm } from './components/LoginForm';
export { ProtectedRoute } from './components/ProtectedRoute';

// Export hooks
export { useAuth } from './hooks/useAuth';
```

### 3. Dynamic Store Configuration

To enable **on-demand** feature loading, the root store should be configured to accept a map of reducers. This allows you to conditionally include features based on environment variables or feature flags.

**Example: `src/app/store.ts`**

```typescript
import { configureStore } from '@reduxjs/toolkit';
import { featureFlags } from '../config/featureFlags';

// Import reducers dynamically
import authReducer from '../features/auth';
import productsReducer from '../features/products';
import cartReducer from '../features/cart';
Comment on lines +139 to +141

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the public API pattern (index.ts), the reducers are exported as named exports (e.g., export { default as authReducer } from './slices/authSlice'). Importing them as default imports here will result in undefined at runtime. They should be imported as named imports instead.

Suggested change
import authReducer from '../features/auth';
import productsReducer from '../features/products';
import cartReducer from '../features/cart';
import { authReducer } from '../features/auth';
import { productsReducer } from '../features/products';
import { cartReducer } from '../features/cart';


// Map of available reducers
const allReducers = {
auth: authReducer,
products: productsReducer,
cart: cartReducer,
};

// Filter reducers based on feature flags
const enabledReducers = Object.keys(allReducers).reduce((acc, key) => {
const featureKey = key as keyof typeof allReducers;
if (featureFlags[featureKey]) {
acc[featureKey] = allReducers[featureKey];
}
return acc;
}, {} as Record<string, any>);
Comment on lines +151 to +157

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Typing enabledReducers as Record<string, any> causes Redux Toolkit's configureStore to lose type safety, resulting in RootState being inferred as Record<string, any>. Typing it as Partial<typeof allReducers> preserves TypeScript type safety for the store's state.

Suggested change
const enabledReducers = Object.keys(allReducers).reduce((acc, key) => {
const featureKey = key as keyof typeof allReducers;
if (featureFlags[featureKey]) {
acc[featureKey] = allReducers[featureKey];
}
return acc;
}, {} as Record<string, any>);
const enabledReducers = Object.keys(allReducers).reduce((acc, key) => {
const featureKey = key as keyof typeof allReducers;
if (featureFlags[featureKey]) {
acc[featureKey] = allReducers[featureKey];
}
return acc;
}, {} as Partial<typeof allReducers>);


export const store = configureStore({
reducer: enabledReducers,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false, // Adjust as needed for your async logic
}),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
```

### 4. Feature Flags Configuration

Centralize your feature toggles to easily enable or disable entire modules without code changes in the logic itself.

**Example: `src/config/featureFlags.ts`**

```typescript
// In a real app, these might come from an API or environment variables
export const featureFlags = {
auth: true,
products: true,
cart: process.env.REACT_APP_ENABLE_CART === 'true', // Example of env-based toggle
};
```

## Benefits of This Structure

* **Scalability**: As the application grows, you simply add new folders to `features/`. The existing code remains untouched.
* **Maintainability**: When a bug occurs in the "Cart" feature, you only need to look in `src/features/cart`. You don't need to search through a massive `actions/` or `reducers/` folder.
* **Reusability**: Because features export a clean API and encapsulate their dependencies, the `auth` feature can be copied into a different project and wired up with minimal effort.
* **Performance**: By conditionally loading reducers (and potentially code-splitting components), you can reduce the initial bundle size for users who don't need certain features.

## Summary Checklist

- [ ] Are all feature-specific files (components, hooks, slices) grouped in a single feature folder?
- [ ] Does each feature have an `index.ts` that acts as a public API?
- [ ] Are shared utilities and generic components kept in a separate `shared/` folder?
- [ ] Is the root store configured to dynamically include/exclude reducers?
- [ ] Can a feature be disabled via a configuration flag without breaking the app?