Is Container vs Presentational Dead? What Survived the Hooks Era

Jul 05, 2026
11 min read
Is Container vs Presentational Dead? What Survived the Hooks Era

Somewhere around 2019, "container vs presentational components" got quietly filed under obsolete. Hooks had landed, the man who popularized the pattern had publicly walked it back, and every other blog post declared that containers/ and components/ folders were a relic. Move on, we have useEffect now.

That take was half right, and the half it got wrong is the interesting half. The folder convention deserved to die. The idea it was pointing at — separate the component that knows about the world from the component that just renders — didn't die at all. It went underground, changed shape, and quietly became one of the most useful instincts you can have. It's the exact split I kept landing on in the composition and DI article without naming it. Time to name it.

What the pattern actually said

For anyone who missed the original: the classic pattern split components into two kinds.

Container components knew how the app worked. They fetched data, held state, talked to the store, ran side effects — and rendered almost nothing themselves. Presentational components knew how things looked. They took props, rendered markup, fired callbacks, and knew nothing about where their data came from or where it went.

// the old shape — a container that fetches, and a dumb view it feeds class UserListContainer extends React.Component { state = { users: [], loading: true }; componentDidMount() { fetchUsers().then((users) => this.setState({ users, loading: false })); } render() { return <UserList users={this.state.users} loading={this.state.loading} />; } } function UserList({ users, loading }) { if (loading) return <Spinner />; return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>; }

The value was real: UserList was trivial to test and reuse because it was just a function of its props, and the messy stateful part lived in one place. The problem was equally real: it was ceremony. Every stateful component needed a class wrapper around a function, entire files existed only to fetch-and-pass, and the two halves were welded together one-to-one anyway. You wrote twice the components for a separation that was mostly bookkeeping.

What hooks actually killed

Then hooks arrived, and here's the precise thing they changed: you no longer need a separate component to hold state or effects. The whole mechanical reason containers existed — "a function component can't have state, so wrap it in a class" — evaporated. A single function component can now fetch, hold state, run effects, and render:

function UserList() { const { data: users, isLoading } = useQuery(['users'], fetchUsers); if (isLoading) return <Spinner />; return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>; }

One component. No container. And this is genuinely better for most cases — the ceremony is gone. So when people say "hooks killed container/presentational," this is what they mean, and they're right: hooks killed the requirement to physically split components just to have state.

But look closely at what they did not kill. UserList above now knows how to fetch. It reaches out and grabs its data — the exact reach-out anti-pattern from last time. It's convenient, and for a component you'll only ever use in one place, convenient is correct. The trouble is that people took "hooks removed the ceremony" and heard "the separation was pointless." Those are not the same sentence.

The idea that refused to die

Strip away the class wrappers and the folder names, and the pattern was never really about containers. It was about a boundary: one side knows about the outside world — data, state, side effects, where things come from and go — and the other side just takes inputs and renders. Hooks removed the boilerplate for drawing that boundary. They did not remove the reasons you'd want to.

You still want it every single time you need to:

  • test rendering without a backend — a pure view takes props, no mock server required;
  • reuse the same look with different data — the same table fed by a query, a websocket, or fixtures;
  • show it in Storybook — a component that fetches its own data can't live in a story without a whole app around it;
  • let a designer or a different feature reuse the visuals without inheriting your data layer.

So the modern move isn't containers/ folders. It's this: write the one-component version by default, and split along the data/render seam the moment you need the render half to stand on its own. Same UserList, split not because a rule said so but because you now have a reason:

// the "container" is just a function that fetches and delegates — no class, no folder function UserListView({ users, isLoading }: { users: User[]; isLoading: boolean }) { if (isLoading) return <Spinner />; return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>; } function UserList() { const { data = [], isLoading } = useQuery(['users'], fetchUsers); return <UserListView users={data} isLoading={isLoading} />; }

Notice there's no class, no containers/ directory, no naming ritual. Just a component that knows about the world wrapping a component that doesn't. The pattern survived; the ceremony didn't. That's the whole story of the "death" of container/presentational.

Where hooks made it fuzzier — and better

The honest update is that hooks also gave us a third option the old pattern didn't have, and it's often the best one: extract the world-knowledge into a custom hook instead of a component.

// the "container logic" is now a hook, not a wrapper component function useUserList() { const { data = [], isLoading } = useQuery(['users'], fetchUsers); return { users: data, isLoading }; } function UserList() { const { users, isLoading } = useUserList(); if (isLoading) return <Spinner />; return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>; }

Now the "smart" part is reusable on its own — any component can call useUserList — and the rendering stays simple. This is the container/presentational split turned sideways: instead of smart component + dumb component, it's smart hook + component that renders. For a lot of cases this is the sweet spot, because the reusable unit you actually wanted was the data logic, not a wrapper component. The boundary is still there. It just moved from a component seam to a hook seam.

So the real menu today isn't "container or not." It's three options, and you pick by what you need to reuse or isolate:

  1. One component that does it all — the default, for anything single-use.
  2. Smart hook + component — when the logic is what you want to reuse or test.
  3. Fetching component + pure view — when the rendering is what you want to reuse, test, or hand to Storybook.

Server Components made the split matter again

Here's the twist nobody saw coming when they were busy declaring the pattern dead. React Server Components brought back a hard, physical version of exactly this boundary — and this time the framework enforces it.

A Server Component runs on the server, can be async, can hit the database directly, and ships no JavaScript to the browser. A Client Component ('use client') runs in the browser and is the only thing that can hold state or handle events. The natural, idiomatic shape is: Server Components fetch and compose the data; Client Components take that data as props and handle interactivity.

// app/users/page.tsx — Server Component: the "container", now literally server-side export default async function UsersPage() { const users = await db.user.findMany(); // no client JS, runs on the server return <UserList users={users} />; } // UserList.tsx — 'use client': the interactive, presentational half 'use client'; export function UserList({ users }: { users: User[] }) { const [query, setQuery] = useState(''); const shown = users.filter((u) => u.name.includes(query)); return (/* search box + list */); }

Read that and tell me it isn't container/presentational wearing a new uniform. The server component knows about the world — it fetches, it composes. The client component takes props and renders, plus handles the interactivity that has to live in the browser. The pattern everyone buried in 2019 is now baked into the newest thing React ships, except now the boundary isn't a convention you can ignore — it's a wall the runtime draws for you, and crossing it wrong is a build error.

What I actually believe about it

The pattern isn't dead and it isn't alive in its original form either. What happened is more useful: the rule dissolved and the instinct got sharper. I don't make containers/ folders. I don't wrap functions in classes. I don't split a component just because a style guide from 2016 says smart and dumb should be separate files.

What I do — every time — is keep one question live while I write a component: does this thing know about the world, or does it just render? When the answer is "both, and I need one of those halves on its own," I split along that seam, and I reach for whichever tool fits — a hook, a pure view, or a Server/Client boundary. The genius of the old pattern was never the folders. It was teaching a generation to notice that seam at all. That part was never obsolete. It just stopped needing a name to be worth doing.

That closes out this run on architecture and boundaries — where code lives, which way it depends, how to size and wire a component, and now where the smart/dumb line actually falls. Next I want to move from structure to patterns: the reusable-component techniques — compound components, headless UIs, behavior-as-API — that decide whether the things you build get used or quietly rewritten. If you've still got a containers/ folder in a codebase somewhere, don't rename it in a panic — just notice whether the split it's drawing is one you'd still draw today. Tell me what you find; I'd bet half of them are pulling their weight and half are pure ceremony.

Found this helpful?

Let's discuss your project needs.

Get in touch