DEV Community

Cover image for RSC and the Echo of 'Presentational and Container Components'
Lorenzo Rivosecchi
Lorenzo Rivosecchi

Posted on

RSC and the Echo of 'Presentational and Container Components'

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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} />;
  }
}

Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)