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