ProNextJS
    Loading
    lesson

    DIY Streaming with Server Actions

    Jack HerringtonJack Herrington

    Suspense and granular loading can be used to fetch and display data. Let's take it up a notch by introducing server actions to create a dynamic UI streaming experience.

    From React Server Components to Server Actions

    Our dashboard currently relies on a React server component that calls a getStocks function to fetch data. Let's refactor this to use a server action instead.

    First, let's create our server action at src/get-stocks-action.ts:

    // app/actions/getStocksAction.ts
    
    "use server";
    import { getStocks } from "@/lib/getStocks";
    
    export async function getStocksAction() {
      return await getStocks();
    }
    

    This server action will handle fetching our stock data. Pretty straightforward, right?

    Now, let's adjust our dashboard component. Since we're working with server actions, we'll need to convert it into a client component and remove the async keyword. We'll also initialize our stock state using useState, but instead of storing an array of stock information, we'll store an array of promises that resolve to stock information.

    // inside app/dashboard.tsx
    
    "use client";
    import { Suspense, use, useState, useEffect } from "react";
    
    // existing StockDisplay function
    
    export default function Dashboard() {
      const [stocks, setStocks] = useState<StockInfo[]>([]);
      ...
    

    Next, let's invoke our server action within a useEffect hook. This will fetch the stock data when the component mounts.

    import { getStocksAction } from "./get-stocks-action";
    
    // inside Dashboard component
    
    // existing code
    
    useEffect(() => {
      getStocksAction().then((stocks) => setStocks(stocks));
    }, []);
    
    return (
      <div className="grid grid-cols-3 gap-4 mt-5">
        {stocks.map((stockPromise, index) => (
          <Suspense key={index} fallback={<div>Loading...</div>}>
            <StockDisplay stockPromise={stockPromise} />
          </Suspense>
        ))}
      </div>
    );
    

    With this current setup, an Unhandled Runtime Error can occur:

    Unhandled Runtime Error

    The error message tells us that the stock-with-counter.tsx module isn't in the React Client Manifest. This happens because Next.js is looking at the client components and server components to determine what needs to be sent to the client. Since stock-with-counter.tsx is called indirectly through the server action, Next.js might determine it's unused and omit it from the client manifest, causing errors. By assigning it to a variable, we ensure it's included.

    A hack to get around this is to import the component somewhere and set a variable to it, even if you don't use it:

    import StockWithCounter from "@/components/stock-with-counter";
    const foo = StockWithCounter;
    

    This setup avoids the tree-shaking issue and ensures the component is included in the client manifest, and the dashboard loads as expected:

    the stock dashboard is loading

    Adding Dynamic Refresh Functionality

    Now we have interactive components and streaming working, but it still requires a manual refresh to get the latest data. Let's add a button to dynamically refetch the stock information.

    We'll create a function called callStockAction to handle this.

    // inside app/dashboard.tsx
    
    const callStocksActions = async () => getStocksAction().then((stocks) => setStocks(stocks));
    

    Finally, we'll add a button to our JSX that calls callStockAction when clicked:

    
    // inside Dashboard component
    
    return (
      <>
        {/* ... (Existing JSX) */}
    
        <button
          onClick={() => callStocksActions()}
          className="..."
        >
          Refresh
        </button>
        ...
    

    With this in place, we have a refresh button that triggers our server action and dynamically updates the displayed stock data.

    The Power of Next.js App Router

    This example demonstrates the amazing capabilities of Next.js's App Router and its server actions. We've created a dynamic, interactive stock dashboard with seamless UI streaming, all without writing complex data fetching logic.

    This DIY streaming approach, where a single server request streams back UI and data, offers a user experience unmatched by other frameworks.

    As you go deeper into Next.js development, consider how you can utilize these features in your own applications!

    Transcript

    So I've taken stocks with suspense granular and turned it into stocks with server actions, but I haven't made any changes yet. Let's go and take a look at where we were with our code. So over here in the dashboard, we're exactly where we left off before. We've got a React server component called dashboard that invokes get stocks. Now what happens if we want to make that get stocks a server action instead?

    Let's go create the server action first. I'll call this get stocks action and in this action all we're gonna do is await get stocks. So we're gonna move the awaiting of get stocks from this React server function into this server action. Okay, the first thing we know is we need to be a client component, So let's make this into a client component. That's not going to work because we got to get rid of this async.

    So now we're going to need to invoke that server action and then store those stocks somewhere. So I'm certain I need useState to store that. So let's bring in useState. And the stockState isn't going to be an array of stockInfos, it's actually going to be an array of promises to stockInfos. All right.

    So far, so good. So now let's bring in our server action. And we want to invoke that. So let's get a use effect going. So bring in use effect.

    And then down here, we will call it. All right, let's see what we got. All right, so to start off, we get a bunch of loadings, and then we get this interesting behavior where we actually get this issue where it says that the stock with counter is not in the React client manifest. So what's happening here is that Next.js is looking at the client components and server components and saying, well, what needs to go out to the client? And it can't see really anybody using stock with counter directly.

    You're going through the server action and that's calling this other function and that other function is then getting that stock with that counter. So it's losing track of it and it's saying, well, I don't need it. So it's not actually sending out to the client and therefore it's not in the client manifest. That's what that error means. So one way to get around that is to just import that stock with counter somewhere.

    And let's see if that helps. Nope, same thing. But here's a little hack for you. If you run into this, what you can do is you can do something with it. So I'm just gonna set a local variable, let's say foo.

    I'm not even gonna use it and I'm just gonna set it to stock with counter and that's going to avoid the tree shaking which is gonna remove stock with counter. So let's see if that works. And there we go. Now we're starting to get our response back. Look at that.

    Look at how easy it is to do this streaming. And these are completely interactive components. But now we've got essentially the same functionality here. I have to refresh in order to get the new stock stuff. What if I actually want to just do that dynamically?

    What if I want to have a button that then does that refetch? How hard is that? So let's go over to our dashboard and I'm going to take this section and I'm going to call that, call stock action. And it's going to be an async function that then does that, and now we can call that down in our use effect. And then down in our JSX, let's go make a button.

    We'll give it some nice tailwind styling and we'll call it refresh. And then when you click it we'll do call stock actions and you know what let's go and set the stocks to an empty array before we do that. All right, looks like everything's good, nice. Let's hit refresh. How cool is that?

    Look at how that's streaming in like that. And we haven't really done all that much. And we're getting UI back from server actions. It's incredibly powerful. And when you think about it, This is really DIY streaming.

    If I look back here at what's actually happening, if I clear my console and I hit refresh, it's loading up everything and then we're seeing that it's finished the post. So we make one post to the server, and then we stream back out on that same connection, the UI, the data, all of it. It is fantastic. I really hope you use features like this in your Next.js app router application. This is the power of the Next.js app router framework.

    We had nothing like this in the Pages router and when you look at other frameworks that are starting to support RSCs and starting to support server actions, to be honest, none of them has really come up to this level of power quite yet. So this is something is currently exclusive just to the App Writer and you should really think about how you can make use of it in your applications to provide a much more rich user experience.