Lesson 70 of 80
Splitting 'what data and logic' from 'how it renders' — hooks made the wrapper component optional, not the split itself.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Split a piece of UI into two roles: a container that owns data, state, and logic, and a presentational component that only knows how to render whatever props it's handed. The presentational component doesn't fetch, doesn't know where its data came from, and doesn't call setState for anything beyond its own purely visual concerns (like "is this dropdown open"). It's a function of its props, full stop.
This pattern predates hooks — it was originally solved with two separate components, one wrapping the other. Hooks didn't kill the idea; they just removed the requirement that the "container" half be a component at all. You can now put the data-and-logic half in a custom hook and call it from wherever you need it, with no extra wrapper in the tree.
The original shape — two components, one wrapping the other:
// Presentational — pure, only cares about rendering
function UserList({ users, onSelect }) {
return (
<ul>
{users.map((u) => (
<li key={u.id} onClick={() => onSelect(u.id)}>{u.name}</li>
))}
</ul>
);
}
// Container — owns the data and the logic
function UserListContainer() {
const [users, setUsers] = useState([]);
const [selectedId, setSelectedId] = useState(null);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
return <UserList users={users} onSelect={setSelectedId} />;
}
Now the same split, with the container's logic moved into a custom hook instead of a wrapper component:
function useUsers() {
const [users, setUsers] = useState([]);
const [selectedId, setSelectedId] = useState(null);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
return { users, selectedId, onSelect: setSelectedId };
}
function UserListPage() {
const { users, onSelect } = useUsers();
return <UserList users={users} onSelect={onSelect} />;
}
UserList didn't change at all. That's the point: the presentational half was never the part hooks made obsolete. What disappeared is the extra component in the tree whose only job was holding state and calling useEffect — useUsers does that without needing its own slot in the render tree.
React.memo. Since it's meant to be a pure function of props with no internal data-fetching, its output is fully determined by what's passed in — exactly the property memoization needs to be safe and effective.The props lesson establishes that a component's output should be predictable from its inputs — that's what makes a component testable and reusable in the first place. The presentational half of this pattern is that same idea, deliberately enforced as a boundary: everything that would make output unpredictable (network calls, timers, external state) is pushed onto the container or the hook, leaving a component you can render with fixture data and get the exact same result every time.