Back to all notes

Smart vs Dumb Components: A Practical Hierarchy for Features and UI

Keep feature logic in smart components and render with simple, reusable UI components.

2 min read

Whenever I let a page component own a feature’s state and its markup at the same time, neither piece ends up reusable. So I split them. Smart components manage data and interactions, dumb components just render the UI, and the feature gets assembled from simple visual building blocks.

Everything inside App

The tangled version has App holding the article, the like handler, and every line of markup.

import { useState } from "react";

function App() {
  const [article, setArticle] = useState({
    title: "Understanding React Patterns",
    content: "React patterns help us write better, more maintainable code...",
    likes: 0,
  });

  const handleLike = () => {
    setArticle((prev) => ({ ...prev, likes: prev.likes + 1 }));
  };

  return (
    <div>
      <h1>Smart vs Dumb Components</h1>
      <h2>Understanding React Patterns</h2>
      <p>React patterns help us write better, more maintainable code...</p>
      <button onClick={handleLike}>Like ({article.likes})</button>
    </div>
  );
}

export default App;

There’s no card to take anywhere, just this page. Behavior and UI are glued together, so nothing gets reused until the whole thing is rewritten.

Card, Button, and ArticleFeature

The fix is a Card and a Button that know nothing about articles, plus an ArticleFeature that knows everything and hands the UI only what it needs.

import { useState } from "react";

function Card({ title, content }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
}

function Button({ children, onClick }) {
  return <button onClick={onClick}>{children}</button>;
}

function ArticleFeature() {
  const [article, setArticle] = useState({
    title: "Understanding React Patterns",
    content: "React patterns help us write better, more maintainable code...",
    likes: 0,
  });

  const handleLike = () => {
    setArticle((prev) => ({ ...prev, likes: prev.likes + 1 }));
  };

  return (
    <div>
      <Card title={article.title} content={article.content} />
      <Button onClick={handleLike}>Like ({article.likes})</Button>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Smart vs Dumb Components</h1>
      <ArticleFeature />
    </div>
  );
}

export default App;

Card and Button get built with no state. article and handleLike move into ArticleFeature. Card receives exactly title and content, Button receives handleLike, and App is left placing the feature on the page.

After the split, Card and Button stay stateless and reusable anywhere. ArticleFeature owns behavior and orchestration. Pages compose features, which compose UI.

A few habits keep it that way. UI components stay pure and style-focused, no side effects. If a UI component starts holding state, it’s becoming a feature, and the state moves up. Repeated UI patterns get extracted early. In larger apps that pays off quickly.