ProNextJS
    Loading
    lesson

    Access Data with React Server Components

    Jack HerringtonJack Herrington

    Currently, there's no way to access any of the previous chats in our application. A great solution to this problem would be to create a list of previous chats at the top of the page.

    Before we start implementing this feature, let's do some refactoring on our current code since there will be two different mechanisms for showing chats.

    Creating a Transcript Component

    In our existing Chat component, the messages section is a bit complex. Let's extract it into its own component so that we can reuse it between new chats and existing chats.

    Create a new file components/Transcript.tsx, which will contain much of the existing messages section from the Chat component, along with some additional functionality.

    This new Transcript component will receive a list of messages and a truncate option as props. The truncate option will be used to shorten the length of the message if it's too long:

    // inside Transcript.tsx
    
    import { Message } from "@/types";
      
    const truncateText = (str: string, length: number) =>
      str.length > length ? str.slice(0, length) + "..." : str;
    
    export default function Transcript({
      messages,
      truncate = true,
    }: {
      messages: Message[];
      truncate?: boolean;
    }) {
      return (
        <div className="flex flex-col gap-2">
          {messages.map((message, index) => (
            <div
              key={index}
              className={`flex flex-col ${
                message.role === "user" ? "items-end" : "items-start"
              }`}
            >
              <div
                className={`${
                  message.role === "user" ? "bg-blue-500" : "bg-gray-500 text-black"
                } rounded-md py-2 px-8`}
              >
                {truncate ? truncateText(message.content, 200) : message.content}
              </div>
            </div>
          ))}
        </div>
      );
    }
    

    Updating the Chat Component

    After creating the component, we can now import it into our Chat component along with the Message types:

    // inside components/Chat.tsx
    
    import Transcript from "./Transcript";
    import { Message } from "@/types";
    

    Inside of the Chat component's render method, we can replace the previous div that we used for displaying the chat transcript with the Transcript component:

    // inside the Chat component
      return (
        <div className="flex flex-col">
          <Transcript messages={messages} truncate={false} />
          <div className="flex border-t-2 border-t-gray-500 pt-3 mt-3">
            <Input
              className="flex-grow text-xl"
              placeholder="Question"
              ...
    

    For now, we'll have the truncate prop set to false since in the Chat component, we want to be able to see the full text of each message.

    Creating a Previous Chats Component

    Now let's move on to creating a PreviousChats component. This component will be responsible for displaying the chat history or previous conversations. It's a good way to keep track of the conversation flow and all the messages that have been exchanged.

    Create a new file at components/PreviousChats.tsx. This is going to be a React Server Component, which is an asynchronous component that can perform database work or request backend services. It can then send the data to a client component to be displayed.

    Remember, React Server Components and Client Components are both rendered on the server. The main difference is that React server components stop there. They only get rendered on the server. Client components get rendered in both places.

    The PreviousChats component will first get the current user's session using the getServerSession function. Then, it will fetch the existing chat conversations that this user has participated in using the getChatsWithMessages function, passing in the user's email as an argument:

    import { getServerSession } from "next-auth";
    import { getChatsWithMessages } from "@/db";
    
    export default async function PreviousChats() {
      const session = await getServerSession();
      const chats = await getChatsWithMessages(session?.user?.email!);
      ...
    

    Now, we need to account for two different scenarios:

    If the user hasn't participated in any chat conversations before, we'll display a message that says so. If the user does have previous chats, we want to display them in an easy-to-browse format. For each chat conversation, we'll wrap the transcript in a Link component that navigates to a detailed view of that chat.

    We'll use a Transcript component that we built earlier to display the content of the chat.

    To make the list of previous chats more appealing, we'll enclose each chat transcript into a box inside a grid. Each box will contain the chat's name and its transcript, and we'll add a separator between each chat.

    Here's how PreviousChats looks like all together:

    // inside PreviousChats.tsx
    import { getServerSession } from "next-auth";
    import Link from "next/link";
    
    import { Separator } from "@/components/ui/separator";
    
    import { getChatsWithMessages } from "@/db";
    
    import Transcript from "./Transcript";
    
    export default async function PreviousChats() {
      const session = await getServerSession();
      const chats = await getChatsWithMessages(session?.user?.email!);
    
      return (
        <div>
          {chats.length > 0 && (
            <>
              <div className="text-2xl font-bold">Previous Chat Sessions</div>
              <div className="grid grid-cols-1 md:grid-cols-2">
                {chats.map((chat) => (
                  <div key={chat.id} className="m-1 border-2 rounded-xl">
                    <Link
                      href={`/chats/${chat.id}`}
                      className="text-lg line-clamp-1 px-5 py-2 text-white bg-blue-900 rounded-t-lg"
                    >
                      {chat.name}
                    </Link>
                    <div className="p-3">
                      <Transcript messages={chat.messages.slice(0, 2)} />
                    </div>
                  </div>
                ))}
              </div>
              <Separator className="mt-5" />
            </>
          )}
    
          {chats.length === 0 && (
            <div className="flex justify-center">
              <div className="text-gray-500 italic text-2xl">
                No previous chats.
              </div>
            </div>
          )}
        </div>
      );
    }
    

    And that's it! We've now completed a system for displaying previous chat sessions to the user. Based on the user's previous chats, our application can now either inform the user that they have no previous chats or display a nicely formatted list of their past conversations.

    Now we can update the main home page to include the PreviousChats component. We'll also include the NewChatSession component so that the user can start a new chat session:

    // inside app/page.tsx
    
    // other imports as before
    import PreviousChats from "@/components/PreviousChats";
    
    export default async function Home() {
      const session = await getServerSession();
    
      return (
        <main className="p-5">
          <h1 className="text-4xl font-bold">Welcome To GPT Chat</h1>
          {!session?.user?.email && <div>You need to log in to use this chat.</div>}
          {session?.user?.email && (
            <>
              <PreviousChats />
              <h4 className="mt-5 text-2xl font-bold">New Chat Session</h4>
              <Separator className="my-5" />
              <Chat />
            </>
          )}
        </main>
      );
    }
    

    With this change, the previous chats will be displayed on the homepage:

    Previous chats on the homepage

    The homepage fetches a server session and once it identifies who you are, then the PreviousChats component independently reaches out to the database to retrieve existing chats with messages.

    Handling Delays in Database Interaction

    We know that the chats are loaded, but if there's a long delay the page will stay blank while the data is loading. This isn't the best user experience.

    In order to add a loading message or other placeholder while the previous chats are being fetched, we can use Suspense.

    To do this, import Suspense from React and then wrap PreviousChats in a Suspense component. This allows us to provide a fallback state, which we'll set to a "Loading previous chats" message:

    // inside app/page.tsx
    
    import { Suspense } from "react";
    // other imports as before
    
    // inside the Home component return
    
    {session?.user?.email && (
      <>
        <Suspense fallback={<div>Loading Previous Chats</div>}>
          <PreviousChats />
        </Suspense>
        <h4 className="mt-5 text-2xl font-bold">New Chat Session</h4>
        <Separator className="my-5" />
        <Chat />
      </>
    )}
    

    Manually introducing a 3 second timeout to the PreviousChats component will show the "Loading previous chats" message on the homepage while the data is loading, while other UI components are ready to go and interactive.

    the loading message displays

    Out-of-Order Streaming

    The out-of-order streaming behavior provided by Suspense is incredibly powerful. Here's what happens:

    When you request the homepage from a server, it sends back everything that isn't wrapped in Suspense first. It holds the connection open until all the Suspense boundaries are resolved. Then, it takes the output of the Suspense boundaries and streams it to the client. The client goes ahead and updates that content anywhere on the page based on its own schedule. This is why it's referred to as "out-of-order streaming".

    It used to be that you would have to render the page and then pause if you had some promise-type request ongoing and then continue. You couldn't finish the HTML until the whole page was done. However, with out-of-order streaming, you can stream the whole page and change out parts of it using the Suspense system.

    One of the biggest advantages of the App Router system over the Pages Router system comes down to managing laggy components. With the Pages Router system, it was far more involved. You'd have to bail out of getServerSideProps, make requests off the client, open up APIs, and so on. It required a lot of work, potentially opening up a host of security issues.

    With App Router, it's as simple as adding a Suspense. It works wherever you need it, with minimal fuss.

    Now, we'll just remove that timeout and we are all set to push the changes to production. The result is a more robust, efficient, and responsive webpage, providing a smoother user experience. Learning efficient ways to handle component loading with Suspense and understanding the concept of out-of-order streaming is quite powerful when building modern applications.

    Next Step

    In the next lesson, we'll be working with a feature exclusive to the App Router in Next.js. It's called Parallel Routes, and it certainly lives up to its hype!

    Transcript