I’m working on a side-project where I needed to continuesly display data coming in from an ajax streaming endpoint. I needed a Reactjs hook that supports the following:

  • Can handle upgrade response from streaming endpoint
  • Request can be cancelled
  • Continuesly streams the data

I looked around and there are already several fetch hooks out there, one of which use-abortable-fetch supports aborting but it doesn’t support streaming. The api ideally should look the same:

const { data, error, abort } = useAbortableFetch(...);

The difference is that instead of data returning as a one-off once the response is received. This will now continuesly return stream of chunks in byte array that are received from the server. The imlementation looks rather straight-forward:

import { useState, useEffect } from 'react';

interface StreamState {
  data: Uint8Array | null;
  error: Error | null;
  controller: AbortController;
}

const useAbortableStreamFetch = (url: string, options?: RequestInit): {
  data: Uint8Array | null,
  error: Error | null,
  abort: () => void,
} => {

  const [state, setState] = useState<StreamState>({
    data: null,
    error: null,
    controller: new AbortController(),
  });

  useEffect(() => {
    (async () => {
      try {
        const resp = await fetch(url, {
          ...options,
          signal: state.controller.signal,
        });
        if (!resp.ok || !resp.body) {
          throw resp.statusText;
        }

        const reader = resp.body.getReader();
        while (true) {
          const { value, done } = await reader.read();
          if (done) {
            break;
          }

          setState(prevState => ({ ...prevState, ...{ data: value } }));
        }
      } catch (err) {
        if (err.name !== 'AbortError') {
          setState(prevState => ({ ...prevState, ...{ error: err } }));
        }
      }
    })();

    return () => state.controller.abort();
  }, [url, options]);

  return {
    data: state.data,
    error: state.error,
    abort: () => state.controller && state.controller.abort(),
  };
};

export default useAbortableStreamFetch;

First we useState to store intermediate data chunks, then we useEffect that gets run only when url and option changes. From there we just send the request and continuesly read through the stream, each time setting the read chunks into the state so the caller will receive it.

So I went ahead and open a PR to add streaming support. And while that’s still being reviewed, I also made a separate package with similar implementation but solely for streaming useAbortableStreamFetch so that I could use it immediately. I plan on closing it once the PR is accepted, but for now you can use both depending on your use-case.