React Advanced Composition Patterns

Sun Jun 23 2024

React has grown to the point where new architecture for react development has been released in previous years. This kind of significant design change occurred long after React's functional approach was first presented. React's entire new ecosystem now consists of server and client components; server side capabilities can now be leveraged out of the box with the addition of client interactions. By precisely defining a boundary between client and server, server components can fully utilize server resources and integrate with client components with ease.

Recently, I worked on a project that heavily utilizes these React composition principles. Making that project approximately will help us comprehend this composition and the true potential of React. It is important to note that server code is rendered on the server and is never communicated to the client; therefore, when creating a composition, client components can be used directly in server components, but server components cannot be used in client components.

However, server components when passed as a props to client components then they are still rendered on server and the chunk is replaced at client. React composition is the term for this combination of server and client components, which is highly effective in demonstrating how the react behaves when producing this tree.

The project is a straightforward one in which the user may paste markdown content and share the URL to view it. The markdown is stored in a database using a special key index that we can use to retrieve the markdown.

img

The form action is executed when the Generate URL button is hit, saving the markdown in the database and redirecting to the paste URL, which may be shared with anybody to view the markdown content. Let's use programming to demonstrate this.

import { create } from "@/actions";
export default async function NewPaste() {
  return (
  <form action={create}>
    <input name="markdown" required>
    <button>Generate URL</button>
  </form>
  )
}

Remember that everything is server rendered. Now that the paste is completed, the page navigates to the paste and displays the markdown parsed view by retrieving data from the database. Let's see the obtain markdown parsed view page code in action.

import { getPaste } from "@/lib";
import { Markdown } from "markdownlib/server";
export default async function View(id) {
  const { content } = await getPaste(id);
  return <Markdown>{content}</Markdown>;
}

Now that I'm not shipping any bundles to the client, which is what we require in this situation, I can see the parsed form of markdown on the view page. It is rendered quickly on the server.

img

Everything is fine as of right now, but I got a feature request to add a password so that even when the link is shared, only those with the password can view the content. It looks easy enough—I created a new, optional field in the database called password and added it to the creation form along with a new input field called password. Everything went smoothly, and I was also able to save to a database. Check out the code modifications I made.

import { create } from "@/actions";
export default async function NewPaste() {
  return (
  <form action={create}>
    <input name="markdown" required>
    <input name="password">
    <button>Generate URL</button>
  </form>
  )
}
img

In order to show them the markdown on the view page of markdown, I must first obtain their password. If the password is correct, I can show them the markdown. Alternatively, I could create a page that asks for their password and then navigate to the view markdown page. However, that would require passing the password to the page, which would not be safe. Markdown is viewed after the client verifies the password through an api call or server action, in order to achieve this client interaction. Then, if the state containing the password verification information is correct, I display the markdown, but the markdown rendering on the server side is compromised. Allow me to present to you a snapshot of the current state of the code.

"use client";
import { useGetPaste } from "@/lib";
import { Markdown } from "markdownlib/client";
import { verify } from "@/actions";
export default function View(id) {
  const { content, isLoading, isProtected } = useGetPaste(id);
  const [verified, setVerifed] = useState(false);

  if (isLoading) return "loading...";
  if (!verified && isProtected)
    return <VerifyDialog cb={setVerifed} action={verify} />;
  return <Markdown>{content}</Markdown>;
}

The password feature has been added, and everything is functioning as it did before. However, the main issue is that markdown is now rendered on the client, which means I have to ship a library to the client in order to perform the markdown render, which is inefficient.We will now leverage the power of React composition to see how we can accomplish both client interaction for password verification and markdown server side rendering. To do this, let's go back to the View component's old code and make a few adjustments.

import { getPaste } from "@/lib";
import { Markdown } from "markdownlib/server";
export default async function View(id) {
  const { content, isProtected } = await getPaste(id);
  return (
    <Verifier isProtected={isProtected} pasteId={id}>
      <Markdown>{content}</Markdown>
    </Verifier>
  );
}
"use client";
import { verify } from "@/actions";
export function Verifier({ isProtected, pasteId, children }) {
  const [verified, setVerifed] = useState(false);
  if (!verified && isProtected)
    return <VerifyDialog cb={setVerifed} action={verify} />;
  return children;
}

Here, you can observe that the markdown is still rendered by the server but is only visible to the client following password verification. This is an example of how client interaction and server side rendering of a component are achieved. The point is that in order to distinguish between the client and server boundaries and build reliable applications, we must comprehend react's composition and patterns, which are incredibly powerful. A few client components should ideally be kept at the base of your tree at all times.