Skip to main content

Command Palette

Search for a command to run...

What Is the Best Way to Structure React Components?

Learn: What Is the Best Way to Structure React Components?

Updated
β€’11 min readβ€’View as Markdown
T

Welcome to TopperBlog! πŸ‘‹

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: β€’ AI/ML Engineering & LLMs β€’ Web3 & Blockchain Development
β€’ System Design & Architecture β€’ Interview Preparation (FAANG) β€’ Freelancing & Remote Work β€’ Modern Tech Stacks (Next.js, React, Rust, TypeScript) β€’ Performance Optimization & Best Practices

πŸ’Ό Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

πŸ“š 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

What Is the Best Way to Structure React Components?

Introduction

I'll never forget the day I opened a React codebase that made me question my career choices. It was 2019, and I'd just joined a startup as a senior developer. The previous team had left behind what I can only describe as a 3,000-line component that handled everything from API calls to button styling. Scrolling through that file felt like descending into Dante's Infernoβ€”each level more chaotic than the last.

That experience taught me something crucial: how you structure your React components matters just as much as the code itself. A well-architected component is like a well-organized kitchenβ€”you know exactly where everything is, and cooking (or coding) becomes a joy rather than a frustration.

Today, I'm going to share the battle-tested patterns I've learned from building dozens of React applications. Whether you're a beginner wondering why your components feel messy or an experienced developer looking to level up your architecture game, this guide will give you a clear roadmap to structuring React components that scale.

The Problem: When Component Structure Goes Wrong

Let me paint you a familiar picture. You start a new React project, full of enthusiasm. The first component is clean and simple. Then you add a feature. Then another. Before you know it, your UserDashboard.jsx component looks like this:

function UserDashboard() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [filter, setFilter] = useState('');
  const [sortOrder, setSortOrder] = useState('asc');
  const [modalOpen, setModalOpen] = useState(false);
  const [selectedUser, setSelectedUser] = useState(null);

  useEffect(() => {
    fetchUsers();
  }, []);

  const fetchUsers = async () => {
    // 50 lines of API logic
  };

  const handleSort = () => {
    // sorting logic
  };

  const handleFilter = () => {
    // filtering logic
  };

  // ... 200 more lines

  return (
    <div>
      {/* 300 lines of JSX */}
    </div>
  );
}

Sound familiar? This is what I call Component Chaos Syndrome, and it leads to:

  • Impossible debugging: Finding bugs becomes like searching for a needle in a haystack
  • Reusability nightmare: You can't reuse anything because everything is tangled together
  • Testing hell: Writing tests for a 500-line component is nobody's idea of fun
  • Team friction: Your colleagues will struggle to understand and modify your code
  • Performance issues: React has to re-render everything when anything changes

The good news? There's a better way, and it's not even that complicated once you understand the core principles.

The Foundation: Component Architecture Principles

Before we dive into specific patterns, let's establish the fundamental principles that guide good React component structure:

1. Single Responsibility Principle

Each component should do one thing and do it well. If you can't describe what your component does in a single sentence without using "and," it's probably doing too much.

2. Separation of Concerns

Keep your business logic, presentation, and side effects separate. This isn't just theoreticalβ€”it makes your code dramatically easier to test and maintain.

3. Composition Over Inheritance

React embraces composition. Instead of creating complex inheritance hierarchies, build small components and compose them together like LEGO blocks.

4. Predictable Data Flow

Data should flow in one direction (top-down), making it easy to trace where state comes from and how it changes.

The Container-Presentational Pattern

This is the pattern that saved my sanity on that chaotic project I mentioned earlier. The idea is beautifully simple: separate components that manage logic from components that handle presentation.

Container Components (Smart Components)

These components are concerned with how things work:

// UserDashboardContainer.jsx
import { useState, useEffect } from 'react';
import { fetchUsers, deleteUser } from '../api/userService';
import UserDashboardView from './UserDashboardView';

function UserDashboardContainer() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    loadUsers();
  }, []);

  const loadUsers = async () => {
    setLoading(true);
    try {
      const data = await fetchUsers();
      setUsers(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleDeleteUser = async (userId) => {
    await deleteUser(userId);
    loadUsers();
  };

  return (
    <UserDashboardView
      users={users}
      loading={loading}
      error={error}
      onDeleteUser={handleDeleteUser}
    />
  );
}

export default UserDashboardContainer;

Presentational Components (Dumb Components)

These components are concerned with how things look:

// UserDashboardView.jsx
function UserDashboardView({ users, loading, error, onDeleteUser }) {
  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage message={error} />;

  return (
    <div className="dashboard">
      <h1>User Dashboard</h1>
      <UserList users={users} onDelete={onDeleteUser} />
    </div>
  );
}

export default UserDashboardView;

Why this works:

  • Presentational components are easy to testβ€”just pass props and check the output
  • You can reuse presentational components with different data sources
  • Logic changes don't affect UI, and UI changes don't affect logic
  • Your components become self-documenting

The Custom Hooks Pattern

Custom hooks are my secret weapon for extracting and reusing component logic. They let you separate what your component does from how it looks.

Before: Logic Mixed with UI

function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);
  const [page, setPage] = useState(1);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/products?page=${page}`)
      .then(res => res.json())
      .then(data => {
        setProducts(data);
        setLoading(false);
      });
  }, [page]);

  return (
    // JSX
  );
}

After: Logic Extracted to Custom Hook

// hooks/useProducts.js
function useProducts(page) {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchProducts = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/products?page=${page}`);
        const data = await response.json();
        setProducts(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchProducts();
  }, [page]);

  return { products, loading, error };
}

// ProductList.jsx
function ProductList() {
  const [page, setPage] = useState(1);
  const { products, loading, error } = useProducts(page);

  if (loading) return <Spinner />;
  if (error) return <Error message={error} />;

  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
      <Pagination page={page} onPageChange={setPage} />
    </div>
  );
}

Benefits of custom hooks:

  • Reusable logic across multiple components
  • Easier to test in isolation
  • Cleaner component code
  • Better separation of concerns

The Compound Components Pattern

This pattern is perfect when you need components that work together but want to give users flexibility in how they compose them. Think of it like a puzzle where the pieces fit together naturally.

// Accordion.jsx
const AccordionContext = createContext();

function Accordion({ children }) {
  const [openIndex, setOpenIndex] = useState(null);

  return (
    <AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
}

function AccordionItem({ index, children }) {
  const { openIndex, setOpenIndex } = useContext(AccordionContext);
  const isOpen = openIndex === index;

  return (
    <div className="accordion-item">
      {React.Children.map(children, child =>
        React.cloneElement(child, { isOpen, onToggle: () => setOpenIndex(isOpen ? null : index) })
      )}
    </div>
  );
}

function AccordionHeader({ children, isOpen, onToggle }) {
  return (
    <button className="accordion-header" onClick={onToggle}>
      {children}
      <span>{isOpen ? 'βˆ’' : '+'}</span>
    </button>
  );
}

function AccordionPanel({ children, isOpen }) {
  return isOpen ? <div className="accordion-panel">{children}</div> : null;
}

Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;

export default Accordion;

Usage:

<Accordion>
  <Accordion.Item index={0}>
    <Accordion.Header>What is React?</Accordion.Header>
    <Accordion.Panel>React is a JavaScript library...</Accordion.Panel>
  </Accordion.Item>
  <Accordion.Item index={1}>
    <Accordion.Header>Why use React?</Accordion.Header>
    <Accordion.Panel>React makes building UIs easier...</Accordion.Panel>
  </Accordion.Item>
</Accordion>

This pattern gives you flexibility while maintaining a clear relationship between components.

The Atomic Design Pattern

Atomic Design, popularized by Brad Frost, organizes components into a hierarchy inspired by chemistry. I've found this particularly useful for large applications with design systems.

The Five Levels

1. Atoms - Basic building blocks (buttons, inputs, labels)

// components/atoms/Button.jsx
function Button({ children, variant = 'primary', ...props }) {
  return (
    <button className={`btn btn-${variant}`} {...props}>
      {children}
    </button>
  );
}

2. Molecules - Simple groups of atoms (search bar, form field)

// components/molecules/SearchBar.jsx
function SearchBar({ onSearch }) {
  const [query, setQuery] = useState('');

  return (
    <div className="search-bar">
      <Input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <Button onClick={() => onSearch(query)}>Search</Button>
    </div>
  );
}

3. Organisms - Complex components (navigation bar, product card grid)

// components/organisms/ProductGrid.jsx
function ProductGrid({ products, onAddToCart }) {
  return (
    <div className="product-grid">
      {products.map(product => (
        <ProductCard 
          key={product.id}
          product={product}
          onAddToCart={onAddToCart}
        />
      ))}
    </div>
  );
}

4. Templates - Page layouts without real data

5. Pages - Specific instances of templates with real data

Folder Structure Example

src/
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ atoms/
β”‚   β”‚   β”œβ”€β”€ Button/
β”‚   β”‚   β”œβ”€β”€ Input/
β”‚   β”‚   └── Label/
β”‚   β”œβ”€β”€ molecules/
β”‚   β”‚   β”œβ”€β”€ SearchBar/
β”‚   β”‚   └── FormField/
β”‚   β”œβ”€β”€ organisms/
β”‚   β”‚   β”œβ”€β”€ Header/
β”‚   β”‚   β”œβ”€β”€ ProductGrid/
β”‚   β”‚   └── Footer/
β”‚   β”œβ”€β”€ templates/
β”‚   β”‚   └── MainLayout/
β”‚   └── pages/
β”‚       β”œβ”€β”€ HomePage/
β”‚       └── ProductPage/

Feature-Based Structure

As your application grows, organizing by feature rather than by component type can make more sense. This is the structure I use for most medium-to-large applications.

src/
β”œβ”€β”€ features/
β”‚   β”œβ”€β”€ authentication/
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ LoginForm.jsx
β”‚   β”‚   β”‚   └── SignupForm.jsx
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   └── useAuth.js
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   └── authService.js
β”‚   β”‚   └── index.js
β”‚   β”œβ”€β”€ products/
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductList.jsx
β”‚   β”‚   β”‚   β”œβ”€β”€ ProductCard.jsx
β”‚   β”‚   β”‚   └── ProductDetail.jsx
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   └── useProducts.js
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   └── productService.js
β”‚   β”‚   └── index.js
β”‚   └── cart/
β”‚       β”œβ”€β”€ components/
β”‚       β”œβ”€β”€ hooks/
β”‚       └── services/
β”œβ”€β”€ shared/
β”‚   β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ hooks/
β”‚   └── utils/
└── App.jsx

Why I love this structure:

  • Everything related to a feature lives together
  • Easy to find and modify feature-specific code
  • Teams can work on different features without conflicts
  • Easier to remove or refactor entire features
  • Scales well as your app grows

The Render Props Pattern

While hooks have largely replaced this pattern, render props are still useful for certain scenarios, especially when building libraries or dealing with complex component composition.

// DataFetcher.jsx
function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [url]);

  return render({ data, loading, error });
}

// Usage
function UserProfile() {
  return (
    <DataFetcher
      url="/api/user/profile"
      render={({ data, loading, error }) => {
        if (loading) return <Spinner />;
        if (error) return <Error message={error.message} />;
        return <ProfileCard user={data} />;
      }}
    />
  );
}

Comparison Table: Component Structure Patterns

PatternBest ForComplexityReusabilityLearning Curve
Container-PresentationalSeparating logic from UILowHighEasy
Custom HooksReusable stateful logicLow-MediumVery HighEasy
Compound ComponentsFlexible, related componentsMediumMediumMedium
Atomic DesignDesign systems, large teamsMedium-HighHighMedium
Feature-BasedLarge applicationsMediumMediumEasy
Render PropsLibrary components, complex compositionMedium-HighHighMedium-Hard

Best Practices for Component Structure

1. Keep Components Small and Focused

If your component file is over 200 lines, it's probably time to break it down. I use this rule of thumb: one component, one responsibility.

2. Use Meaningful Names

// ❌ Bad
function UC() { }
function Data() { }

// βœ… Good
function UserCard() { }
function UserProfileData() { }

Keep component files, styles, and tests together:

Button/
β”œβ”€β”€ Button.jsx
β”œβ”€β”€ Button.test.jsx
β”œβ”€β”€ Button.module.css
└── index.js

4. Establish Clear Prop Interfaces

Use PropTypes or TypeScript to document what props your components expect:

import PropTypes from 'prop-types';

function UserCard({ user, onEdit, onDelete }) {
  // component logic
}

UserCard.propTypes = {
  user: PropTypes.shape({
    id: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    email: PropTypes.string.isRequired,
  }).isRequired,
  onEdit: PropTypes.func,
  onDelete: PropTypes.func,
};

5. Avoid Prop Drilling

When you're passing props through multiple levels, consider using Context API or state management:

// ❌ Prop drilling
<App>
  <Dashboard user={user}>
    <Sidebar user={user}>
      <UserMenu user={user} />
    </Sidebar>
  </Dashboard>
</App>

// βœ… Context
const UserContext = createContext();

function App() {
  const [user, setUser] = useState(null);

  return (
    <UserContext.Provider value={user}>
      <Dashboard>
        <Sidebar>
          <UserMenu />
        </Sidebar>
      </Dashboard>
    </UserContext.Provider>
  );
}

6. Optimize Performance Strategically

Don't optimize prematurely, but know your tools:

  • React.memo() for expensive presentational components
  • useMemo() for expensive calculations
  • useCallback() for stable function references
  • Code splitting with React.lazy() for large components
// Memoize expensive components
const ProductCard = React.memo(function ProductCard({ product }) {
  return (
    <div className="product-card">
      {/* expensive rendering */}
    </div>
  );
});

// Lazy load heavy components
const AdminDashboard = React.lazy(() => import('./AdminDashboard'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <AdminDashboard />
    </Suspense>
  );
}

FAQ Section

What's the ideal size for a React component?

There's no magic number, but I follow the "scroll test": if you can't see the entire component on your screen without scrolling, it's probably too large. Generally, aim for components under 200 lines. More importantly, if a component does multiple things or has multiple reasons to change, split it up regardless of size.

Should I always separate container and presentational components?

Not always. For simple components that don't have much logic, combining them is fine. The pattern shines when you have complex business logic or want to reuse the same UI with different data sources. Use it when it adds value, not dogmatically.

How do I decide between custom hooks and component composition?

Use custom hooks when you want to reuse stateful logic across different components. Use component composition when you want to reuse UI elements or create flexible component APIs. If you're extracting logic that doesn't render anything, it's probably a hook. If you're extracting something that renders UI, it's probably a component.

Is it okay to have components that are just wrappers around other components?

Absolutely! Wrapper components are great for adding consistent styling, behavior, or context to existing components. For example, wrapping a third-party component to match your design system or adding analytics tracking. Just make sure each wrapper adds clear value.

How do I structure components when using TypeScript?

TypeScript enhances component structure by making prop interfaces explicit. I recommend defining interfaces for all component props, using discriminated unions