Table of Contents
- Project Organization: Folder Structure
- Component Structure and Composition
- State Management Strategies
- Routing Best Practices
- Styling Approaches
- Code Splitting and Performance Optimization
- Testing React Applications
- Documentation
- Tooling for Consistency and Quality
- Conclusion
- References
1. Project Organization: Folder Structure
A clear folder structure is the foundation of a maintainable React app. It helps developers navigate the codebase, locate files quickly, and understand how features are organized. Two popular approaches are component-based (grouping by type) and feature-based (grouping by functionality). For most large applications, feature-based organization is preferred, as it colocated related code (components, logic, tests) for a specific feature.
Feature-Based vs. Component-Based
-
Component-Based: Groups files by type (e.g.,
components/,hooks/,services/). Simple for small apps but becomes messy as features grow (files for a single feature spread across multiple folders).src/ ├── components/ │ ├── Button.jsx │ ├── Navbar.jsx ├── hooks/ │ ├── useAuth.js ├── services/ │ ├── api.js -
Feature-Based: Groups files by feature (e.g.,
auth/,dashboard/), with shared code inshared/. Ideal for large apps, as all code for a feature lives in one place.src/ ├── features/ │ ├── auth/ │ │ ├── components/ │ │ │ ├── LoginForm.jsx │ │ │ ├── SignupForm.jsx │ │ ├── hooks/ │ │ │ ├── useLogin.js │ │ ├── services/ │ │ │ ├── authApi.js │ │ ├── tests/ │ │ │ ├── LoginForm.test.jsx │ │ ├── AuthPage.jsx │ │ ├── index.js // Exports public API for the feature │ ├── dashboard/ │ │ ├── ... ├── shared/ │ ├── components/ // Reusable UI components (Button, Card) │ ├── hooks/ // Shared hooks (useLocalStorage, useFetch) │ ├── utils/ // Helper functions (formatDate, validateEmail) │ ├── assets/ // Images, fonts, global CSS ├── App.jsx ├── index.jsx
Key Folders in Feature-Based Structure
-
features/: Contains all feature-specific code. Each feature folder includes:components/: Feature-specific components (not shared).hooks/: Custom hooks for the feature.services/: API calls or external service integrations.tests/: Tests for the feature.index.js: Exports public components/hooks (encapsulates internal files).
-
shared/: Reusable code across features (e.g.,Button,useFetch,formatCurrency). -
routes/: Route definitions (if using a centralized routing setup). -
store/: Global state (e.g., Redux slices, Zustand stores).
Why It Matters: Reduces cognitive load by keeping related code together. New developers can quickly grasp feature boundaries, and refactoring is easier (delete a feature folder without hunting for scattered files).
2. Component Structure and Composition
React components are the building blocks of your app. Well-structured components are reusable, testable, and easy to debug.
Prefer Functional Components with Hooks
Since React 16.8, functional components with hooks have replaced class components as the standard. Hooks (e.g., useState, useEffect, useContext) simplify state management and lifecycle logic.
Example: Functional Component vs. Class Component
// ❌ Avoid: Class component (verbose, harder to reuse logic)
class Counter extends React.Component {
state = { count: 0 };
increment = () => this.setState({ count: this.state.count + 1 });
render() {
return <button onClick={this.increment}>{this.state.count}</button>;
}
}
// ✅ Prefer: Functional component with hooks (concise, reusable logic)
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(prev => prev + 1);
return <button onClick={increment}>{count}</button>;
};
Separation of Concerns: Presentational vs. Container Components
-
Presentational Components: Focus on UI (how things look). They accept props and render markup. Stateless (or minimal state).
// Presentational Component (shared/Button.jsx) const Button = ({ label, onClick, variant = "primary" }) => ( <button className={`btn btn-${variant}`} onClick={onClick}> {label} </button> ); -
Container Components: Focus on logic (how things work). They fetch data, manage state, and pass props to presentational components. Often use hooks or global state.
// Container Component (features/auth/LoginPage.jsx) const LoginPage = () => { const [email, setEmail] = useState(""); const { login, isLoading } = useAuth(); // Custom hook for auth logic const handleSubmit = (e) => { e.preventDefault(); login(email); }; return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /> <Button label={isLoading ? "Logging in..." : "Login"} disabled={isLoading} /> </form> ); };
Component Composition
Avoid “god components” (large, multi-purpose components). Instead, compose smaller components using:
-
Children Props: Pass nested content to a component (e.g.,
CardwithCard.HeaderandCard.Body).const Card = ({ children }) => <div className="card">{children}</div>; const CardHeader = ({ children }) => <div className="card-header">{children}</div>; // Usage <Card> <CardHeader>Welcome</CardHeader> <p>Hello, world!</p> </Card> -
Compound Components: Expose a set of related components (e.g.,
Tabs,Tabs.List,Tabs.Panelfrom Radix UI) to create flexible UIs. -
Higher-Order Components (HOCs) or Custom Hooks: Reuse logic across components (e.g.,
withAuthHOC for protected routes, oruseAuthhook).
Avoid Prop Drilling: Passing props through multiple levels of components. Use Context API or global state (e.g., Redux) for shared state, or extract logic into custom hooks.
3. State Management Strategies
State management is critical for handling data flow in React apps. Choose the right tool based on your app’s complexity.
Local State vs. Global State
-
Local State: Use for component-specific data (e.g., form inputs, UI toggles) with
useStateoruseReducer.const Form = () => { const [formData, setFormData] = useState({ name: "", email: "" }); // ... }; -
Global State: Use for data shared across components (e.g., user auth, app theme, shopping cart). Popular tools:
- Zustand/Jotai: Lightweight, minimal boilerplate (good for small to medium apps).
- Redux Toolkit: Mature, battle-tested (good for large apps with complex state logic).
- Context API: Built into React, but avoid for frequent state updates (can cause performance issues).
Best Practices for Global State
-
Normalize Data: Store complex data (e.g., from APIs) in a flat structure (like a database) for easier access and updates.
// ❌ Avoid: Nested data (hard to update) const state = { users: [{ id: 1, name: "Alice", posts: [{ id: 1, title: "Hello" }] }] }; // ✅ Prefer: Normalized data (easy to update a post by ID) const state = { users: { 1: { id: 1, name: "Alice", postIds: [1] } }, posts: { 1: { id: 1, title: "Hello", userId: 1 } } }; -
Minimize Global State: Only put data in global state if multiple components need it. Overusing global state bloats the store and complicates debugging.
4. Routing Best Practices
React Router is the de facto standard for routing in React apps. Organize routes logically to reflect your app’s structure.
Key Routing Practices
-
Centralize Route Definitions: Define routes in a single file (e.g.,
src/routes.jsx) for clarity.// src/routes.jsx import { createBrowserRouter } from "react-router-dom"; import Home from "./features/home/HomePage"; import Login from "./features/auth/AuthPage"; export const router = createBrowserRouter([ { path: "/", element: <Home /> }, { path: "/login", element: <Login /> }, ]); -
Dynamic Routes: Use URL parameters for dynamic content (e.g.,
/users/:id).import { useParams } from "react-router-dom"; const UserProfile = () => { const { id } = useParams(); // Accesses `id` from URL // Fetch user data for `id` }; -
Protected Routes: Restrict access to authenticated users with a wrapper component.
const ProtectedRoute = ({ element }) => { const { isAuthenticated } = useAuth(); return isAuthenticated ? element : <Navigate to="/login" />; }; // Usage in routes { path: "/dashboard", element: <ProtectedRoute element={<Dashboard />} /> } -
Route-Based Code Splitting: Load components dynamically to reduce initial bundle size (see Section 6).
5. Styling Approaches
Styling in React can be done with CSS modules, styled components, or utility-first frameworks like Tailwind. The goal is to scope styles, avoid conflicts, and maintain consistency.
Scoped Styles with CSS Modules
CSS Modules (.module.css) scope styles to a component by generating unique class names, preventing global conflicts.
Example:
/* Button.module.css */
.button {
padding: 8px 16px;
border: none;
}
.primary {
background: blue;
color: white;
}
// Button.jsx
import styles from "./Button.module.css";
const Button = ({ variant = "primary" }) => (
<button className={`${styles.button} ${styles[variant]}`}>
Click me
</button>
);
Utility-First with Tailwind CSS
Tailwind provides utility classes (e.g., px-4, bg-blue-500) to build UIs directly in JSX. It’s fast and enforces consistency but can make JSX verbose. Use @apply to extract repeated patterns.
Theming
Use CSS variables or styled-components’ ThemeProvider to enforce a design system (e.g., colors, spacing).
/* theme.css */
:root {
--color-primary: #007bff;
--spacing-sm: 8px;
}
/* Usage */
.button {
background: var(--color-primary);
padding: var(--spacing-sm);
}
Avoid Global CSS: Global styles (e.g., app.css) are hard to maintain. Use scoped styles or utility classes instead.
6. Code Splitting and Performance Optimization
Large bundle sizes slow down app load times. Code splitting and performance optimizations ensure your app is fast and responsive.
Code Splitting with React.lazy and Suspense
Load non-critical components only when needed (e.g., on route change).
import { lazy, Suspense } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
// Dynamically import the component (loaded only when the route is visited)
const Dashboard = lazy(() => import("./features/dashboard/Dashboard"));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</Router>
);
Avoid Unnecessary Re-renders
-
React.memo: Memoize components to prevent re-renders if props haven’t changed.const UserCard = React.memo(({ user }) => <div>{user.name}</div>); -
useCallback/useMemo: Memoize functions and values to avoid recreating them on every render.const handleClick = useCallback(() => { /* ... */ }, [deps]); const sortedList = useMemo(() => data.sort(), [data]);
Optimize Bundle Size
- Tree Shaking: Remove unused code with tools like Webpack or Vite (ensure
package.jsonhas"sideEffects": falsefor libraries). - Analyze Bundles: Use
webpack-bundle-analyzerto identify large dependencies and split them.
7. Testing React Applications
Testing ensures your app works as expected and prevents regressions.
Unit and Component Tests
Use Jest (test runner) and React Testing Library (component testing) to test components in isolation.
Example: Testing a Button Component
import { render, screen, fireEvent } from "@testing-library/react";
import Button from "./Button";
test("calls onClick when clicked", () => {
const handleClick = jest.fn();
render(<Button label="Click" onClick={handleClick} />);
fireEvent.click(screen.getByText("Click"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
Integration and E2E Tests
- Integration Tests: Test interactions between components (e.g., form submission flow).
- E2E Tests: Use Cypress or Playwright to test the app as a user would (e.g., logging in, navigating to a page).
Test Hooks and State
Test custom hooks with @testing-library/react-hooks.
import { renderHook, act } from "@testing-library/react-hooks";
import useCounter from "./useCounter";
test("increments count", () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
8. Documentation
Well-documented code helps teams collaborate and maintain the app long-term.
Component Documentation with Storybook
Storybook lets you build and document components in isolation. Write stories to showcase component variants and usage.
Example: Story for a Button
// Button.stories.jsx
import Button from "./Button";
export default { title: "Components/Button" };
export const Primary = () => <Button variant="primary">Primary</Button>;
export const Secondary = () => <Button variant="secondary">Secondary</Button>;
README Files and JSDoc
- Feature READMEs: Explain the purpose of a feature, how to use its components, and known limitations.
- JSDoc Comments: Document props, hooks, and functions for IDE support.
/** * A reusable button component. * @param {string} label - Text to display on the button. * @param {() => void} onClick - Callback when clicked. */ const Button = ({ label, onClick }) => <button onClick={onClick}>{label}</button>;
9. Tooling for Consistency and Quality
Tools automate code quality, formatting, and type safety, reducing bugs and ensuring consistency.
ESLint and Prettier
- ESLint: Enforces code quality rules (e.g., no unused variables, prefer
constoverlet). - Prettier: Auto-formats code (indentation, line length) to avoid bikeshedding.
Setup:
// .eslintrc.js
module.exports = {
extends: ["react-app", "prettier"],
rules: { "react/prop-types": "error" }
};
TypeScript
Add type safety to catch errors early. Define interfaces for props, state, and API responses.
interface User {
id: number;
name: string;
}
const UserCard = ({ user }: { user: User }) => <div>{user.name}</div>;
Husky and Pre-Commit Hooks
Run linting/tests before commits to prevent bad code from entering the repo.
# Install Husky
npm install husky --save-dev
npx husky install
npx husky add .husky/pre-commit "npm run lint && npm test"
10. Conclusion
Structuring a React application well is an investment in maintainability, scalability, and developer happiness. By following these best practices—from feature-based folder structures to code splitting and testing—you’ll build apps that are easier to debug, extend, and collaborate on.
Remember, there’s no one-size-fits-all solution. Adapt these practices to your team’s needs and app complexity, but always prioritize clarity and consistency.