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
226 changes: 226 additions & 0 deletions docs/issue-42-[section]-scalable-application.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Scalable Application Structure

Creating a scalable structure for a React-Redux application is crucial for long-term maintainability, team collaboration, and feature evolution. This section outlines a strategy focused on **feature-based architecture**, enabling you to group files by domain, easily add or remove features, toggle them on demand, and ensure reusability across different projects.

## Core Goals

1. **Feature-First Organization**: Group all related code (components, actions, reducers, selectors, types) by feature rather than by file type.
2. **Modularity**: Make features self-contained units that can be added or removed with minimal impact on the rest of the application.
3. **Dynamic Feature Toggling**: Enable or disable features at runtime or build time without refactoring core logic.
4. **Reusability & Pluggability**: Design features as independent packages or modules that can be dropped into different Redux applications.

## Recommended Directory Structure

Instead of the traditional `actions/`, `components/`, `reducers/` folders at the root, organize your `src` directory by **features**.

```text
src/
├── app/ # App-level configuration, providers, global styles
│ ├── store.ts # Redux store configuration
│ ├── App.tsx # Root component
│ └── index.tsx # Entry point
├── features/ # Feature modules
│ ├── auth/ # Authentication feature
│ │ ├── components/ # Auth-specific components
│ │ ├── slices/ # Redux Toolkit slice (actions + reducer)
│ │ ├── hooks.ts # Custom hooks for this feature
│ │ ├── selectors.ts # Feature-specific selectors
│ │ ├── types.ts # TypeScript interfaces/types
│ │ └── index.ts # Public API (exports only what's needed)
│ ├── dashboard/ # Dashboard feature
│ │ ├── components/
│ │ ├── slices/
│ │ ├── hooks.ts
│ │ ├── selectors.ts
│ │ ├── types.ts
│ │ └── index.ts
│ └── settings/ # Settings feature
│ └── ...
├── shared/ # Shared utilities, UI components, types
│ ├── components/ # Generic UI components (Button, Modal)
│ ├── hooks/ # Generic hooks
│ ├── utils/ # Helper functions
│ └── types.ts # Global types
└── config/ # Environment variables, feature flags
└── featureFlags.ts
```

## Implementation Details

### 1. Feature Modules as Self-Contained Units

Each feature in `src/features/` should be a complete unit. It should contain its own Redux slice, components, and types. This isolation ensures that changes to one feature rarely break another.

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

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

interface AuthState {
user: string | null;
isAuthenticated: boolean;
loading: boolean;
}

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

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

export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions;
export default authSlice.reducer;
```

**Example: `features/auth/index.ts` (Public API)**

```typescript
// Only export what external parts of the app need
export { default as authReducer } from './slices/authSlice';
export { loginStart, loginSuccess, loginFailure, logout } from './slices/authSlice';
export { useAuth } from './hooks';
export { selectUser, selectIsAuthenticated } from './selectors';
export type { AuthState } from './types';
```

### 2. Enabling/Disabling Features on Demand

To enable dynamic feature toggling, use a configuration file and conditional rendering or conditional store injection.

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

```typescript
export const FEATURE_FLAGS = {
AUTH: process.env.REACT_APP_ENABLE_AUTH === 'true',
DASHBOARD: process.env.REACT_APP_ENABLE_DASHBOARD === 'true',
SETTINGS: process.env.REACT_APP_ENABLE_SETTINGS === 'true',
};
```

**Conditional Store Injection**

When configuring your Redux store, only include reducers for enabled features.

```typescript
// src/app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { FEATURE_FLAGS } from '../config/featureFlags';

// Import reducers conditionally
import authReducer from '../features/auth/slices/authSlice';
import dashboardReducer from '../features/dashboard/slices/dashboardSlice';
import settingsReducer from '../features/settings/slices/settingsSlice';
Comment on lines +131 to +134

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

Public API Violation

Importing directly from internal paths like ../features/auth/slices/authSlice bypasses the feature's public API (index.ts). This violates the encapsulation and modularity goals described earlier in the guide.

To maintain strict boundaries, always import from the feature's root entry point.

Suggested change
// Import reducers conditionally
import authReducer from '../features/auth/slices/authSlice';
import dashboardReducer from '../features/dashboard/slices/dashboardSlice';
import settingsReducer from '../features/settings/slices/settingsSlice';
// Import reducers conditionally from the public API of each feature
import { authReducer } from '../features/auth';
import { dashboardReducer } from '../features/dashboard';
import { settingsReducer } from '../features/settings';


const reducer = {
...(FEATURE_FLAGS.AUTH && { auth: authReducer }),
...(FEATURE_FLAGS.DASHBOARD && { dashboard: dashboardReducer }),
...(FEATURE_FLAGS.SETTINGS && { settings: settingsReducer }),
};
Comment on lines +136 to +140

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

Issues with Conditional Store Injection

Conditionally injecting reducers using object spreading can lead to runtime and type-safety issues:

  1. Type Safety: If RootState is inferred from store.getState(), the state slices for disabled features will be typed as optional or potentially undefined. If selectors are not written defensively (e.g., using optional chaining like state.auth?.user), the application will crash at runtime when a disabled feature's selector is invoked.
  2. Predictability: Redux store structure is generally expected to be static and predictable.

Recommended Alternatives:

  • Static Reducers with UI-only Toggling: Keep all reducers registered in the store. This ensures type safety and predictable state structure, while feature flags only control UI rendering and action dispatching.
  • Dynamic Reducer Injection: For large-scale applications where bundle size of reducers is a concern, use Redux Toolkit's dynamic reducer injection or a custom Reducer Manager to dynamically inject reducers when the feature is loaded.
Suggested change
const reducer = {
...(FEATURE_FLAGS.AUTH && { auth: authReducer }),
...(FEATURE_FLAGS.DASHBOARD && { dashboard: dashboardReducer }),
...(FEATURE_FLAGS.SETTINGS && { settings: settingsReducer }),
};
const reducer = {
auth: authReducer,
dashboard: dashboardReducer,
settings: settingsReducer,
};


export const store = configureStore({
reducer,
});

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

**Conditional Component Rendering**

In your main `App.tsx`, only render feature components if the flag is active.

```typescript
// src/app/App.tsx
import { FEATURE_FLAGS } from '../config/featureFlags';
import { AuthPage } from '../features/auth/components/AuthPage';
import { Dashboard } from '../features/dashboard/components/Dashboard';
import { Settings } from '../features/settings/components/Settings';

function App() {
return (
<div>
{FEATURE_FLAGS.AUTH && <AuthPage />}
{FEATURE_FLAGS.DASHBOARD && <Dashboard />}
{FEATURE_FLAGS.SETTINGS && <Settings />}
</div>
);
}
Comment on lines +156 to +169

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

Public API Violation & Lack of Code Splitting

There are two main architectural issues with this implementation:

  1. Public API Violation: Importing directly from internal paths like ../features/auth/components/AuthPage bypasses the feature's public API (index.ts). The entry components should be exported from the feature's index.ts and imported from the feature root.
  2. Lack of Code Splitting: Statically importing all features means they will all be bundled together, even if their feature flags are disabled. Using React.lazy and Suspense ensures that disabled features are not loaded by the client, which is crucial for a scalable application.
Suggested change
import { FEATURE_FLAGS } from '../config/featureFlags';
import { AuthPage } from '../features/auth/components/AuthPage';
import { Dashboard } from '../features/dashboard/components/Dashboard';
import { Settings } from '../features/settings/components/Settings';
function App() {
return (
<div>
{FEATURE_FLAGS.AUTH && <AuthPage />}
{FEATURE_FLAGS.DASHBOARD && <Dashboard />}
{FEATURE_FLAGS.SETTINGS && <Settings />}
</div>
);
}
import React, { Suspense, lazy } from 'react';
import { FEATURE_FLAGS } from '../config/featureFlags';
// Lazy load features from their public APIs to enable code-splitting
const AuthPage = lazy(() => import('../features/auth').then(m => ({ default: m.AuthPage })));
const Dashboard = lazy(() => import('../features/dashboard').then(m => ({ default: m.Dashboard })));
const Settings = lazy(() => import('../features/settings').then(m => ({ default: m.Settings })));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<div>
{FEATURE_FLAGS.AUTH && <AuthPage />}
{FEATURE_FLAGS.DASHBOARD && <Dashboard />}
{FEATURE_FLAGS.SETTINGS && <Settings />}
</div>
</Suspense>
);
}


export default App;
```

### 3. Reusability and Pluggability

To make features reusable across different applications:

- **Package as a Library**: Extract a feature into a separate npm package.
- **Use TypeScript Interfaces**: Define clear contracts for props and state.
- **Avoid Hard Dependencies**: Do not import from other features directly. Use dependency injection or context if cross-feature communication is needed.
- **Export a Clean API**: Ensure the `index.ts` of each feature exports only the necessary components, hooks, and types.

**Example: Exporting a Feature as a Reusable Module**

```typescript
// features/auth/index.ts
export { AuthProvider, useAuth } from './components/AuthProvider';
export { authReducer } from './slices/authSlice';
export type { AuthState, AuthActions } from './types';
```

Another application can then install this feature as a dependency:

```bash
npm install @myapp/auth-feature
```

And use it:

```typescript
import { AuthProvider, authReducer } from '@myapp/auth-feature';

// In store config
const store = configureStore({
reducer: {
auth: authReducer,
// ... other reducers
},
});

// In App
<AuthProvider>
<App />
</AuthProvider>
```

## Benefits of This Structure

- **Scalability**: As the app grows, new features are added without cluttering the root directory.
- **Maintainability**: Developers can work on a single feature without worrying about breaking others.
- **Flexibility**: Features can be toggled on/off via environment variables or runtime flags.
- **Reusability**: Features can be extracted and reused in other projects with minimal changes.

## Conclusion

Adopting a feature-based, scalable structure in your React-Redux TypeScript application ensures that your codebase remains clean, modular, and adaptable. By grouping files by feature, enabling dynamic toggling, and designing for reusability, you create a foundation that can grow with your project and team.