cover

Let’s continue

Continuing the in-depth analysis started with the previous article “How to stream data over HTTP using Node and Fetch API”, in this one we will see how to apply HTTP streaming within Next.js the well-known React framework to create complete web applications.

In the previous article, we covered how to:

  • Divide our overall computation into smaller tasks that can return a partial (and consistent) result using async generators
  • Send data chunks over HTTP using Node.js from the Server
  • Use a readable stream from Fetch API to receive the data chunks over HTTP on the Client

Next.js implementation

In Next.js the main implementation steps are:

Take note that the implementation will be based on typescript to make in evidence the objects’ type that we are going to use.

Create a ReadableStream from an async generator

We will create a ReadableStream that fetch data from an async generator using TextEncoder to encode the data chunks.


export const makeStream = <T extends Record<string, unknown>>(generator: AsyncGenerator<T, void, unknown>) => 
{

    const encoder = new TextEncoder();
    return new ReadableStream<any>({
        async start(controller) {
            for await (let chunk of generator) {
                const chunkData =  encoder.encode(JSON.stringify(chunk));
                controller.enqueue(chunkData);
            }
            controller.close();
        }
    });
}

The makeStream function takes in an async generator and return a ReadableStream. As you see we can pass to the ReadableStream’s constructor, a callback function that is used when the stream starts to produce data. Once invoked, to this function is provided a controller object where we can put the fetched data on a queue.

So in summary, it’s taking an async generator of data, encoding each chunk to binary data, and piping that data through a ReadableStream so that the stream can be consumed asynchronously. This allows you to generate data on demand and stream it to the client efficiently without buffering everything in memory. The client can then read from the stream over time as the data becomes available.

Create a specialized Response object

Now we create a custom subclass of Response able to manage a ReadableStream

/**
 * A custom Response subclass that accepts a ReadableStream.
 * This allows creating a streaming Response for async generators.
 */
class StreamingResponse extends Response {

  constructor( res: ReadableStream<any>, init?: ResponseInit ) {
    super(res as any, {
      ...init,
      status: 200,
      headers: {
        ...init?.headers,
      },
    });
  }
}

In the code above the StreamingResponse is the custom Response subclass that accepts a ReadableStream as response body. This allows to return a response able to streaming data from a Next.js Route Handler.

Create a NextJS Route Handler that stream data

We have almost done! To test streaming data, we can create a NextJS Route Handler that handle a GET http request that return a response that fetch, format and encode the data from an async generator that is exactly our earlier implemented StreamingResponse class.

// file: app/api/stream-data/route.ts

type Item = {
  key: string;
  value: string;
}

/**
 * async generator that simulate a data fetch from external resource and
 * return chunck of data every second
 */
async function *fetchItems(): AsyncGenerator<Item, void, unknown> {
  
    const sleep = async (ms: number) => 
        (new Promise(resolve => setTimeout(resolve, ms)))
    
    for( let i = 0 ; i < 10 ; ++i ) {
        await sleep(1000)
        yield {
            key: `key${i}`,
            value: `value${i}`
        }
    }
}

/**
 * Next.js Route Handler that returns a Response object 
 * that stream data from the async generator.
 * 
 */
export async function GET(req: NextRequest ) {

    const stream = makeStream( fetchItems() )
    const response = new StreamingResponse( stream )
    return response
}


Create React component that consume the data chunk over http.

The last step is to create a Client Side React Component to consume and show streamed chunck of data.

As we did in the previous article, we used the Fetch API to create the function streamingFetch, which can handle streaming response from the server using a body reader. We use this function in the useEffect React hook as follows:

// file: app/components/RenderStreamData.tsx

/**
 * Generator function that streams the response body from a fetch request.
 */
export async function* streamingFetch( input: RequestInfo | URL, init?: RequestInit ) {

    const response = await fetch( input, init)  
    const reader  = response.body.getReader();
    const decoder = new TextDecoder('utf-8');
  
    for( ;; ) {
        const { done, value } = await reader.read()
        if( done ) break;

        try {
            yield decoder.decode(value)
        }
        catch( e:any ) {
            console.warn( e.message )
        }
      
    }
}

export default function RenderStreamData() {
  const [data, setData] = useState<any[]>([]);

  useEffect( () => {
    const asyncFetch = async () => {
      const it = streamingFetch( '/api/stream-data') 

      for await ( let value of it ) {
        try {
          const chunk = JSON.parse(value);
          setData( (prev) => [...prev, chunk]);
        }
        catch( e:any ) {
          console.warn( e.message )
        }
      }
    }
    
    asyncFetch()
  }, []);

  return (
    <div>
        {data.map((chunk, index) => (
          <p key={index}>{`Received chunk ${index} - ${chunk.value}`}</p>
        ))}
    </div>
  );
}

Et voilà ✅, we have all the main components to implement succesfully streaming data over http in Next.js.

Take a note 👀:

Streaming approach only makes sense when component consume and render data directly from client side, so in this case you must not use the approach of using Server Components available in Next.js.

Conclusion

In this article we have seen a practical guide to using HTTP streaming for efficient data visualization in Next.js web applications. We have explored how create and customize an instance of ReadableStream, creating a Response object  specialization that accepts it as a result body. To test we have used a NextJS Route Handler. Additionally, to consume data chunk over http, we have developed a client-side React component that is the right way to achieve the benefit to streaming data from a server.

Hope that this knowledge will be helpful, stay tune because the next article will be dedicated to implement http streaming from the amazing Next.js Server Actions. In the meanwhile, enjoy coding! 👋

💻 The code is available on Github 💻

References