-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix: [Section] Scalable Application Structure #328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the public API pattern (
Suggested change
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typing
Suggested change
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| 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? | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The public API example (
index.ts) in the next section attempts to exportselectUserandselectIsAuthenticatedfrom./slices/authSlice. However, these selectors are not defined inauthSlice.ts. Defining them here makes the example complete and functional.