Frontend
medium
mid
How would you separate concerns between data rendering and UI controls
Split into layers: data fetching/state in hooks or container components, pure presentational components that render data via props, and UI controls that raise events upward. Keep rendering components dumb and testable; keep data logic reusable and isolated.
6 min read·~10 min to think through
The goal is each layer has one reason to change — so you can swap a data source, restyle the UI, or rework controls independently.
The three concerns
- Data layer — fetching, caching, mutations, derived/computed state. Lives in custom hooks (
useOrders,useFilteredList) or a container component. Knows nothing about markup. - Rendering layer — presentational components that take data via props and render it. Pure, no fetching, no business logic. Trivial to test and Storybook.
- UI controls — buttons, filters, sort toggles, pagination. They raise events (
onFilterChange,onSortChange) and don't own the data — they're told their current value and report changes upward.
How they connect
jsx
function OrdersPage() {
// data + UI-control state live here (or in a hook)
const { filters, setFilters } = useFilters();
const { data, isLoading } = useOrders(filters);
return (
<>
<OrderControls filters={filters} onChange={setFilters} /> {/* controls: raise events */}
<OrderTable rows={data} loading={isLoading} /> {/* rendering: pure */}
</>
);
}Even better: extract a useOrdersPage() hook so the component is only composition.
Principles
- Data flows down, events flow up. Rendering and control components never reach into the data layer.
- Presentational components are pure functions of props — same props, same output.
- Controls are controlled — their value is a prop, changes are events. The parent owns the state.
- Custom hooks isolate logic — the data layer is reusable and unit-testable without rendering anything.
- The container/page just wires — it's the only place that knows about all three.
Why it pays off
- Swap REST for GraphQL → only the hook changes.
- Redesign the table → only the presentational component changes.
- Test the filtering logic → test the hook, no DOM needed.
- Reuse the table with a different data source → it doesn't care, it takes props.
The smell
A component that fetches data, holds filter state, computes derived values, and renders a 200-line table is four concerns in one file. That's the thing to split.
Follow-up questions
- •How do custom hooks help separate data logic from rendering?
- •What's the difference between a controlled and uncontrolled UI control here?
- •How does this separation make testing easier?
- •When does over-separating become a problem?
Common mistakes
- •Mega-components that fetch, hold state, compute, and render all at once.
- •Presentational components reaching into global state or fetching directly.
- •UI controls owning the data they filter instead of raising events.
- •Over-abstracting tiny components into needless layers.
Performance considerations
- •Pure presentational components memoize cleanly. Isolating data logic in hooks lets you control re-fetch and re-render scope. Beware: lifting all control state into one container can over-render — colocate where possible.
Edge cases
- •Controls whose options depend on the fetched data.
- •Derived state that belongs in the data layer, not the render layer.
- •Shared controls used across multiple data views.
- •Optimistic UI blurring the data/render boundary.
Real-world examples
- •A data table feature: useTableData hook, <Table> presentational component, <TableToolbar> raising filter/sort events.
- •Swapping a REST hook for a React Query hook with zero changes to the table component.
Senior engineer discussion
Seniors frame it as 'one reason to change per layer' and lean on custom hooks as the seam between data and rendering. They note the controlled-control discipline (value down, events up) and caution against over-layering — separation should map to real axes of change, not ceremony.
Related questions
Frontend
Medium
6 min
Frontend
Easy
6 min