ProNextJS
    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

    All right, so now looking at this homepage, it's not clear how I get back to any previous chats. So what we really want to do is right up at the top here, we want a list of the previous chats that I've had with this chat GPT application. So let's go back and do a little bit of refactoring

    first. So now we're going to have two different mechanisms for showing chats. And I'm noticing in our chat component that this messages is a little bit complex. I would maybe want to refactor this into its own thing so we can reuse that between the two different ways of showing chats.

    One here as you're creating the chat and then the previous chats stuff that we're going to build. So let's go and create a transcript component that encapsulates this messages section. So now I've created this transcript component over in components, it takes a list of messages

    as well as a truncate option. The truncate option just truncates the message if they're really long, just nice for a summary. And then inside of that, it's just exactly the same messages stuff that we have before with that truncate option. Now we can use that over in our chat. So bring in that

    transcript component as well as message. And then down here, instead of this big div, we'll go and just use transcript. All right, cool. And we won't truncate it because in this case, in case we're actually having a conversation, we want to see the full text of whatever response

    it gave you. All right, so now we've got that handled. Let's create a previous chats component that then uses that transcript to display the transcripts of some previous conversations with the AI so that you can then go and click on it and go to it and continue your conversation. So let's create a new component called previous chats.

    And this is going to be a React server component. It's going to be asynchronous so you can do any database work or request to back end services that you want, and then you can display the output of that or send that data to a client component if that's what you want to do.

    That's what we did with our chats parameterized route. It too is a React server component. First thing it does is go off to the database to go get all the contents of the chat. And then it sends it on to the chat component, which is a client component. Now it's important

    to know when it comes to React server components versus client components, that both of those React server components and client components are both rendered on the server. The only real difference is that React server components stop there. They only get rendered on the server.

    Client components get rendered in both places. Server components only get rendered on the server. All right, let's go back to our previous chats. So the first thing we want to know is who are you, so we're going to use get server session for that. So we'll get our session, and then we'll get our chats with messages from DB, and then we'll bring in get chats with messages, and we'll

    use the user email to get those chats for that particular person. Now, easy scenario number one is if we don't have any previous chats, we can just put up no previous chats. So if we do have previous chats, we're going to want to go and put up the transcript of each one and then wrap it in

    a link to go and send it to the chat detail page. We'll bring in that transcript component we just created, and then down here, if we actually have chats, so we're going to put up a little bit of text that says that we have our previous chat sessions, and then we're going to put up a nice

    grid that has boxes around each linked section that has the chat name as well as the transcript. And be really nice, let's bring in a separator, and then put a little separator down here. Yeah, that's nice. All right, now let's go bring this

    into our main page. So go back over here, make sure that we go look at page. Yep, there we go, and then we'll bring in previous chats, and then right in here, we'll put in our previous chats as well as a new chat session. Okay, take a look.

    All right, let's go take a look at the homepage, and there you go. We got our previous chat sessions. Now that's really cool, I grant you, because now think about what's happening here. We got our homepage, it's getting a server session, and then once we know who you are,

    running previous chats, and then previous chats is all on its own, going off and going to the database and getting this chats with messages. We don't know. It's opaque to us what previous chats is doing. Previous chats is going and doing this database work for us. But what happens

    if it takes a while? For example, let's just put in a wait in here on a timeout and make it something long. We'll wait a new promise where we'll set a timeout to say three seconds. Now let's go back

    over here. I'm going to go to about blank first, and then I'm going to navigate to localhost 3000, and we can see chug, chug, chug, chug, chug, chug, chug, three seconds and go. And that's not great. What's happening here is we're actually blocking this main page from rendering because this

    previous chats is taking a while. Well, let's say we don't want to do that. We want to put up like a loading thing or a spinner or something like that saying, "Hey, that can go take a while, but we still want you to be able to have your page and interact with it." So what we can do

    is we can bring in a suspense, and we can wrap that previous chats in a suspense. Now we do need to give it a fallback. All right, let's try this again. And now we can see that we

    have this loading previous chats, lasts for three seconds, and we go. But all of the other UI was ready to go and interactive while we were waiting for that promise of the set timeout to be resolved.

    How cool is that? It's incredibly powerful that we can take any component and get this out-of-order streaming behavior. So what does out-of-order streaming mean? Well, what happens is that when

    you request this homepage from a server, it sends back everything that isn't wrapped in a suspense first. And then it holds the connection open until those suspense boundaries resolve. And it takes the output of the suspense boundaries, streams it to the client. The client then goes and updates

    that content anywhere on the page. That's what it means by out-of-order. In older systems, the issue was that you would have to render the page and then stop if you had some kind of promise type request going and then continue on. And you wouldn't be able to finish off that HTML

    until the whole page was done. In this new system, now we can stream things out-of-order. You stream the whole page and you can change out parts and pieces of it using this suspense system.

    It's incredibly powerful. This is one of the huge advantages of the App Router system over the Pages Router system. All you need to do to manage laggy components is to wrap them in a suspense and

    they'll be streamed. Whereas with the Pages Router, it was a lot more involved. You'd have to go and bail out of getServerSideProps and then make requests off the client and open up APIs. It

    was a whole production and it added a whole lot of security issues around your application. It's so much easier with the App Router. As you see, all you need to do is add suspense and it works wherever you need it. All right, let's go and remove that timeout and then we'll push it to production.

    Let's check it out on Versal and let's take a look. There you go. Now we have our chat transcripts and we can go into each one. Awesome. In the next section, we're going to talk about another amazing feature of the App

    Router in Next.js that you won't find on the Pages Router. It's called Parallel Routes. I'm super excited to tell you about it. I'll see you in the next section.