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:
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:
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!