Skip to main content

Command Palette

Search for a command to run...

Thinking in Components — The Skill Nobody Teaches You Properly

Day 2 of 40 — React System Design Series

Updated
9 min read
Thinking in Components — The Skill Nobody Teaches You Properly
R

Hi, I’m Richa — a Senior Frontend Engineer with 5+ years of experience building scalable, production-grade web interfaces for enterprise and consumer applications. I work primarily with React, TypeScript, and modern frontend architectures, focusing on component systems, performance, and maintainability. Most of my experience comes from building real-world products in regulated domains like banking and insurance, where clarity, reliability, and long-term ownership matter more than quick demos. Through this blog, I write about frontend engineering fundamentals, scalable UI design, problem-solving, and the lessons I’ve learned working on large codebases. My goal is to share practical insights — not shortcuts — for developers who want to grow strong engineering foundations. I also mentor early-career developers and strongly believe that curiosity, asking the right questions, and understanding why something works are more important than memorizing tools. If you’re serious about improving as an engineer, you’re in the right place.


Hey everyone, Richa here! 👋

thinking gif

Day 2. Let's talk about something that sounds simple but is actually one of the most asked things in senior interviews.

"How do you decide how to break a UI into components?"

When I first heard that question, I thought — okay easy, I just... make components? I split things up when they feel big?

And then I tried to explain it and realised I had no real framework. I was going on instinct. Instinct built from years of doing it — but still instinct. I couldn't articulate the thinking.

That's a problem in an interview. Because the interviewer isn't asking "can you make components" — they already know you can. They're asking "how do you think about it?"

Today we build that framework. Properly.


Why component design is a senior-level skill

Junior developers make components when something is big or when they need to reuse something.

Senior developers think about components upfront — before writing a single line. They think about:

  • What are the responsibilities here?

  • What changes independently?

  • What will need to be reused?

  • What will make this testable?

  • What will make this readable for the next developer?

Component design is architecture at the small scale. Get it wrong and the codebase slowly becomes impossible to change. Get it right and features take half the time they should because everything is already modular.


The single responsibility principle — one job per component

This is the most important rule. Every component should do one thing.

Not one thing and a little bit of another thing. One thing.

Here's a component that does too much:

// ❌ This component has too many jobs
function UserDashboard() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/user')
      .then(r => r.json())
      .then(data => {
        setUser(data.user);
        setPosts(data.posts);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>Loading...</p>;

  return (
    <div>
      <img src={user.avatar} alt={user.name} />
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
      <h2>Posts</h2>
      {posts.map(post => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
          <span>{post.date}</span>
        </div>
      ))}
    </div>
  );
}

This component is doing four things: fetching data, displaying a user profile, displaying a list of posts, and displaying each post. It's going to be impossible to test, painful to reuse, and someone will cry when they have to change it.

Here's the same UI — broken up properly:

// ✅ Each component has one clear job

// Handles data fetching only
function UserDashboard() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/user')
      .then(r => r.json())
      .then(data => {
        setUser(data.user);
        setPosts(data.posts);
        setLoading(false);
      });
  }, []);

  if (loading) return <LoadingSpinner />;

  return (
    <div>
      <UserProfile user={user} />
      <PostList posts={posts} />
    </div>
  );
}

// Displays user info only
function UserProfile({ user }: { user: User }) {
  return (
    <div>
      <img src={user.avatar} alt={user.name} />
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

// Renders the list only
function PostList({ posts }: { posts: Post[] }) {
  return (
    <div>
      <h2>Posts</h2>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

// Renders one post only
function PostCard({ post }: { post: Post }) {
  return (
    <div>
      <h3>{post.title}</h3>
      <p>{post.body}</p>
      <span>{post.date}</span>
    </div>
  );
}

Same UI. But now:

  • UserDashboard only cares about fetching

  • UserProfile only cares about displaying user info

  • PostList only cares about the list

  • PostCard only cares about one post

Each one can be tested independently. Each one can be reused. Each one can be changed without touching the others.


Composition over inheritance — the React way

In object-oriented programming, you reuse code through inheritance. Class B extends Class A and gets all its behaviour.

React doesn't use inheritance for components. It uses composition — you build complex things by combining simple things.

The tool for composition in React is children.

// A reusable Card shell — doesn't care what's inside
function Card({ children, className }: { children: React.ReactNode; className?: string }) {
  return (
    <div className={`card ${className ?? ''}`}>
      {children}
    </div>
  );
}

// Now you can put anything inside it
function UserCard({ user }: { user: User }) {
  return (
    <Card className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
    </Card>
  );
}

function AlertCard({ message }: { message: string }) {
  return (
    <Card className="alert-card">
      <span>⚠️</span>
      <p>{message}</p>
    </Card>
  );
}

Card knows nothing about users or alerts. It just provides a styled shell. The specific content is composed in from outside.

This is infinitely more flexible than inheritance. You never have to worry about deep class hierarchies or which parent class does what.

💡 Interview answer: "React uses composition over inheritance. Instead of extending components, we combine them — passing content via props and children. This keeps components decoupled, reusable, and easy to reason about."


How to actually decide where to split

Here's the framework I use now. Ask these four questions:

1. Does this piece of UI change for different reasons?

If part of a component changes when the user scrolls but another part only changes when data loads — those are two different responsibilities. Split them.

2. Could I reuse this somewhere else?

A Button, a Modal, a LoadingSpinner, an Avatar — these show up everywhere. They should be their own components from the start. Don't copy-paste, extract.

3. Would this be easier to test if it was separate?

If testing this component requires setting up a lot of unrelated state or mocking a lot of unrelated things — it's doing too much. A good component test should be focused and simple.

4. Is it getting hard to read?

A component longer than ~100 lines is usually doing too many things. Not a hard rule — but a strong signal. If you have to scroll to understand one component, something should probably be extracted.


The "too small" trap — don't over-split

Here's the other mistake. Going too far the other way.

// ❌ This is absurd
function UserName({ name }: { name: string }) {
  return <h1>{name}</h1>;
}

function UserAvatar({ src, alt }: { src: string; alt: string }) {
  return <img src={src} alt={alt} />;
}

function UserBio({ bio }: { bio: string }) {
  return <p>{bio}</p>;
}

function UserProfile({ user }: { user: User }) {
  return (
    <div>
      <UserAvatar src={user.avatar} alt={user.name} />
      <UserName name={user.name} />
      <UserBio bio={user.bio} />
    </div>
  );
}

A component that wraps a single <h1> is not useful. It adds file count, adds import overhead, and makes the codebase harder to navigate — without adding any real value.

The rule: extract when you have a reason. Reuse, complexity, testing, independent change. Not just because something exists.


Container vs presentational components

This pattern is worth knowing — especially for interviews.

Presentational components only care about how things look. They receive data via props and render it. No API calls, no state (or minimal UI state like "is this open?"). Pure display.

Container components care about how things work. They fetch data, manage state, and pass it down to presentational components.

// Presentational — just renders what it receives
function ProductCard({ name, price, image, onAddToCart }: ProductCardProps) {
  return (
    <div className="product-card">
      <img src={image} alt={name} />
      <h3>{name}</h3>
      <p>${price}</p>
      <button onClick={onAddToCart}>Add to Cart</button>
    </div>
  );
}

// Container — manages data and behaviour
function ProductCardContainer({ productId }: { productId: string }) {
  const [product, setProduct] = useState<Product | null>(null);

  useEffect(() => {
    fetchProduct(productId).then(setProduct);
  }, [productId]);

  if (!product) return <ProductCardSkeleton />;

  return (
    <ProductCard
      name={product.name}
      price={product.price}
      image={product.image}
      onAddToCart={() => addToCart(product)}
    />
  );
}

ProductCard is easy to test — give it props, assert the output. No API mocking, no async setup.

ProductCardContainer handles all the messy data stuff, so ProductCard stays clean.

💡 Important note: With custom hooks, this split is often done differently today — the container logic lives in a hook (useProduct) instead of a container component. We'll cover that properly on Day 18. But the mental model of separating logic from display is still exactly right.


A real-world example — designing a Comment Section

Let's say you're asked to build a comment section for a blog post. How do you break it down?

Start with the UI:

  • A list of comments

  • Each comment has an avatar, username, timestamp, and text

  • A form to add a new comment

  • A loading state while comments load

Here's how I'd design the components:

CommentSection          ← orchestrates everything, fetches data
  ├── CommentList       ← renders the list
  │     └── CommentCard ← renders one comment (avatar, name, text, time)
  ├── AddCommentForm    ← handles the input and submit
  └── LoadingSpinner    ← shown while loading (reusable)
function CommentSection({ postId }: { postId: string }) {
  const { comments, loading, addComment } = useComments(postId);

  if (loading) return <LoadingSpinner />;

  return (
    <div>
      <CommentList comments={comments} />
      <AddCommentForm onSubmit={addComment} />
    </div>
  );
}

Clean. Each component has one job. CommentSection doesn't know what a comment looks like. CommentCard doesn't know how to fetch anything. Everything is testable in isolation.


Quick summary — the framework

Question If yes →
Does this change for different reasons? Split it
Could I reuse this elsewhere? Extract it
Is it hard to test in isolation? Break it up
Is it over 100 lines and hard to read? Something should be extracted
Does it wrap only 1-2 HTML elements with no real logic? Probably too small, leave it inline

Today's takeaway

Component design is not about making things smaller — it's about making things focused. One job per component, logic separated from display, composed together rather than tangled together.

The next time you're about to write a big component, stop for 60 seconds. Draw out the tree first. Name each node. Ask what each one's job is. That 60 seconds will save you hours later.

nailed it gif

Day 3 tomorrow — useState vs useReducer. When simple state stops being simple.



Day 1 — How React Actually Works Day 3 — useState vs useReducer


Part of the React System Design Series — 40 days, one topic per day.

React System Design — Learning in Public

Part 2 of 34

I'm a React developer with 5.5 years of experience who spent 2 years in Lit.js — and slowly forgot how React really works at a senior level. This series is my public journey back. Every day I cover one topic: state management, authentication, performance, system design interviews, testing — the real stuff that separates a mid-level dev from a senior. Written honestly, with real code, for developers at every level.

Up next

useState vs useReducer — When Simple State Stops Being Simple

Day 3 of 40 — React System Design Series