Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 4 min read

Would You Rather Have a Dumb Component or a Smart Component?

A Component Doesn't Have to Be Dumb to Be Reusable There's a long-running idea in frontend architecture: Keep components dumb. Let them receive data, render the UI, and leave everything else to something higher up th

A Component Doesn't Have to Be Dumb to Be Reusable

There's a long-running idea in frontend architecture:

Keep components dumb.

Let them receive data, render the UI, and leave everything else to something higher up the tree.

It sounds clean. And often, it is.

But there's a trade-off we don't always consider:

Making a component simpler can make the surrounding architecture more complicated.

Let's look at both approaches.

πŸ”΄ The Problem

Imagine a component that displays a list of users.

It needs to fetch the data, filter the results, and render them.

One approach is to put everything inside UserList.

function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [query, setQuery] = useState("");

  useEffect(() => {
    getUsers().then(setUsers);
  }, []);

  const filteredUsers = useMemo(
    () => users.filter((user) => user.name.includes(query)),
    [users, query],
  );

  return (
    <section>
      <input value={query} onChange={(event) => setQuery(event.target.value)} />

      {filteredUsers.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </section>
  );
}

This is a smart component.

It knows where its data comes from, manages its own state, handles filtering, and decides how everything is displayed.

Using it is simple:

<UserList />

But what happens when another part of the application already has the users?

Or when the same UI needs to display data from a different source?

Now the component's built-in behavior becomes a limitation.

🟒 The Other Extreme

We could separate those responsibilities.

The container handles the data. The presentational component handles the UI.

function UserListContainer() {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    getUsers().then(setUsers);
  }, []);

  return <UserList users={users} />;
}

function UserList({ users }: { users: User[] }) {
  return (
    <section>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </section>
  );
}

Now UserList doesn't care where the data comes from.

It can render users fetched by a parent, loaded from a cache, or passed in by a test.

That's a real benefit.

But splitting components isn't free.

The container needs to pass data down. Additional behavior may require additional props. And if the presentation component is only ever used in one place, the extra layer might not make the code any easier to understand.

We haven't necessarily removed complexity. We've moved it.

πŸ”΅ Reusability Has a Cost

A dumb component is often easier to reuse because it depends on fewer things.

It doesn't need to know about an API, a router, a global store, or a particular data-fetching library.

But reusability isn't the only thing that matters.

Sometimes, keeping behavior close to the UI makes a component easier to understand.

Consider a search box.

If its value, filtering logic, and displayed results always belong together, extracting every small piece into separate components and hooks can make a simple feature harder to follow.

On the other hand, if multiple screens need the same filtering behavior or visual list, separating those responsibilities may be worthwhile.

The useful boundary isn't necessarily between smart and dumb components.

It's between responsibilities that belong together and responsibilities that benefit from being independent.

🟣 So Where Should the Logic Live?

There isn't one rule that works for every component.

A few questions can help:

  • Does the component need to work with different data sources? Separating data access from presentation may help.
  • Does the behavior only make sense for this component? Keeping it local may make the code clearer.
  • Is the component coordinating several unrelated concerns? That may be a sign it has too many responsibilities.
  • Are you extracting something just to make a component smaller? Smaller doesn't automatically mean simpler.

You can also mix the approaches.

A component can own its local interaction state while receiving data from a parent. It can use a shared hook for behavior without becoming purely presentational.

These aren't mutually exclusive architectures.

🟠 What About Consistency Across a Codebase?

So far, we've looked at individual components.

But components don't always exist in isolation. They're part of a larger codebase, often maintained by multiple developers.

Imagine a team decides that components should follow a consistent architecture:

  • Components don't fetch data directly.
  • Shared state is accessed through established abstractions.
  • Business logic lives in hooks, services, or other dedicated modules.
  • Components receive the data they need through props.

There are good reasons to do this.

Developers know where to look for things. Responsibilities are predictable. Moving between different parts of the application feels familiar.

But there's a trade-off.

Some components might genuinely be simpler if they handled their own behavior.

Forcing them into the same structure could mean creating unnecessary hooks, adding extra layers, or splitting closely related logic across several files.

You gain consistency, but potentially lose simplicity.

And that's not necessarily a bad decision.

In a large team, predictable code can be more valuable than making every individual component follow its locally simplest implementation.

The important distinction is between consistent principles and identical implementations.

A codebase can have clear architectural rules without forcing every component to have exactly the same structure.

After all, you're maintaining a codebase, not just individual components.

⚑ The Takeaway

Smart and dumb components aren't competing goals.

A dumb component can be wonderfully reusable, but separating every bit of behavior can create unnecessary indirection.

A smart component can be convenient and cohesive, but accumulating unrelated responsibilities can make it difficult to change.

Neither approach is inherently better.

Don't optimize for dumb components. Don't optimize for smart components.

Optimize for clear responsibilities.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.