Introduction
In 2015 Dan Abramov published an article titled Presentational and Container Components.
In the article he suggested a pattern for creating reusable React application by grouping components in two categories.
Presentational (or Dumb) Components
These components have the sole responsibility of rendering UI. Data, Behavior and Logic should be injected by props.
// classes/src/user-profile/presentational.jsx
export default function UserProfile({ user, handleLike }) {
return (
<section>
<h3>
{user.firstName} {user.lastName}
</h3>
<p>{user.bio}</p>
<button onClick={handleLike}>Like</button>
<span>{user.likes}</span>
</section>
);
}
Container (or Smart) Components
These components are the polar opposite. They should handle Data, Behavior and Logic, and delegate rendering to other components.
// classes/src/user-profile/container.jsx
import { Component } from "react";
import { getUser, likeUser } from "../../../shared/user";
import Presentational from "./presentational";
export default class UserProfile extends Component {
state = { user: null };
constructor() {
super();
this.handleLike = this.handleLike.bind(this);
this.revalidate = this.revalidate.bind(this);
}
componentDidMount() {
this.revalidate();
}
async revalidate() {
const user = await getUser();
if (!user) return;
this.setState({ user });
}
async handleLike() {
const user = this.state.user;
if (!user) return;
await likeUser(user.id);
this.revalidate();
}
render() {
const user = this.state.user;
if (!user) return <div>Loading...</div>;
return <Presentational user={user} handleLike={this.handleLike} />;
}
}
React Hooks
As mentioned in the original article, since the introduction of Hooks (2018) this pattern is not recommended anymore.
That's because hooks are the chosen primitive for sharing Data, Behavior and Logic between components.
In the hooks version we inject data and behavior by calling special functions that start with use
in the component body.
// hooks/src/user-profile/component.jsx
import { useUserProfile } from "./hooks";
export default function UserProfile() {
const { user, handleLike } = useUserProfile();
if (!user) return <div>Loading...</div>;
return (
<section>
<h3>
{user.firstName} {user.lastName}
</h3>
<p>{user.bio}</p>
<button onClick={handleLike}>Like</button>
<span>{user.likes}</span>
</section>
);
}
Here is how the hooks are defined:
// hooks/src/user-profile/hooks.jsx
import { getUser, likeUser } from "../../../shared/user";
import { useState, useCallback, useEffect } from "react";
export function useUser() {
const [user, setUser] = useState();
const revalidate = useCallback(() => {
getUser().then(setUser);
}, []);
useEffect(revalidate, [revalidate]);
return { user, revalidate };
}
export function useLikeUser() {
return useCallback(likeUser, []);
}
export function useUserProfile() {
const { user, revalidate } = useUser();
const likeUser = useLikeUser();
const handleLike = useCallback(async () => {
if (!user) return;
await likeUser(user.id);
revalidate();
}, [user]);
return {
user,
handleLike,
};
}
React Server Components
Fast forward to 2023, we are all learning React Server Components: A new paradigm that brings the component model to the Server.
With Server components we can create async components that are rendered only on the server. One of the main benefits of this approach is that developers can fetch data at the component level without worrying about creating expensive network waterfalls. The goal of the React team is to make it easier to consume data in our applications without reducing performance.
When i was experimenting with Server Components i found myself doing Presentational and Container components once again.
The server component handles data fetching and mutations using async/await
and Server Actions
// rsc/app/page.jsx
import { getUser, likeUser } from "../../shared/user";
import Presentational from "./Presentational";
// In RSC revalidation is handled by the Framework (Next.js)
import { revalidatePath } from "next/cache";
export default async function UserProfile() {
const user = await getUser();
async function handleLike() {
"use server";
await likeUser(user.id);
revalidatePath("/");
}
return <Presentational user={user} handleLike={handleLike} />;
}
The client component, receives props and renders markup.
"use client";
// rsc/app/Presentational.jsx
import { useTransition } from "react";
export default function UserProfile({ user, handleLike }) {
const [isPending, startTransition] = useTransition();
return (
<section>
<h3>
{user.firstName} {user.lastName}
</h3>
<p>{user.bio}</p>
<button
onClick={() => {
startTransition(handleLike);
}}
>
Like
</button>
<span>{user.likes}</span>
</section>
);
}
Conclusions
I haven't used this approach in a real application, therefore i'm not sure that this pattern has legs.
In fact, rendering markup only on client components defeats one of the most important benefits of RSC: Shipping less JavaScript to the browser.
I decided to write this article because this case reminded me of how in Software Engineering ideas circulate and change in small but powerful ways. 🌻
Github Repository for this article.
Top comments (0)