User Guide
Tutorials
Fullstack - FastAPI/React
Simple Frontend

Implementing Real-time Progress Streaming with Hatchet and React

In this tutorial, we'll walk through how to build a single-page React application that streams real-time progress updates from a Hatchet workflow. We'll cover how to:

  1. Set up a new React project using Create React App
  2. Send a request to the FastAPI server to trigger a workflow
  3. Subscribe to the Hatchet event stream for the workflow run
  4. Display the real-time progress updates in the React UI

Set up a new React project

First, let's create a new React project using Create React App. Open your terminal and navigate to the directory where you want to create your project. Then, run the following command:

npx create-react-app frontend --template typescript

This will create a new directory called frontend with a basic React project set up using TypeScript.

Navigate to the frontend directory and start the development server:

cd frontend
npm start

Open your browser and navigate to http://localhost:3000. You should see the default Create React App page.

Set up the React component

Now, let's set up the basic structure of our React component. Open the src/App.tsx file and replace its contents with the following code:

import { useEffect, useState } from "react";
import "./App.css";
 
interface Messages {
  role: "user" | "assistant";
  content: string;
  messageId?: string;
}
 
const API_URL = "http://localhost:8000";
 
function App() {
  const [openRequest, setOpenRequest] = useState<string>();
  const [message, setMessage] = useState<string>("");
  const [messages, setMessages] = useState<Messages[]>([
    { role: "assistant", content: "How can I help you?" },
  ]);
  const [status, setStatus] = useState("idle");
 
  // ... rest of the component code
}
 
export default App;

Trigger the Hatchet workflow

Next, let's create a function to send a request to the FastAPI server to trigger the Hatchet workflow:

const sendMessage = async (content: string) => {
  try {
    setMessages((prev) => [...prev, { role: "user", content }]);
 
    const response = await fetch(`${API_URL}/message`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        url: "https://docs.hatchet.run/home",
        messages: [
          ...messages,
          {
            role: "user",
            content,
          },
        ],
      }),
    });
 
    if (response.ok) {
      // Handle successful response
      setOpenRequest((await response.json()).messageId);
    } else {
      // Handle error response
    }
  } catch (error) {
    // Handle network error
  }
};

In this step, we create a function that sends a POST request to the FastAPI server with the user's message and the URL of the documentation page. If the response is successful, we set the openRequest state to the messageId returned by the server.

Subscribe to the Hatchet event stream

Now, let's use the useEffect hook to subscribe to the Hatchet event stream for the workflow run:

useEffect(() => {
  if (!openRequest) return;
 
  const sse = new EventSource(`${API_URL}/message/${openRequest}`, {
    withCredentials: true,
  });
 
  function getMessageStream(data: any) {
    console.log(data);
    if (data === null) return;
    if (data.payload?.status) {
      setStatus(data.payload?.status);
    }
    if (data.payload?.message) {
      setMessages((prev) => [
        ...prev,
        {
          role: "assistant",
          content: data.payload.message,
          messageId: data.messageId,
        },
      ]);
      setOpenRequest(undefined);
    }
  }
 
  sse.onmessage = (e) => getMessageStream(JSON.parse(e.data));
 
  sse.onerror = () => {
    setOpenRequest(undefined);
    sse.close();
  };
 
  return () => {
    setOpenRequest(undefined);
    sse.close();
  };
}, [openRequest]);

In this step, we use the EventSource API to subscribe to the event stream for the openRequest. We define a getMessageStream function to handle the incoming events. If the event contains a status update, we set the status state. If the event contains the final message, we add it to the messages state and clear the openRequest state.

Render the UI

Finally, let's render the UI with the messages and the real-time progress updates:

return (
  <div className="App">
    <div className="Messages">
      {messages.map(({ role, content, messageId }, i) => (
        <p key={i}>
          <b>{role === "assistant" ? "Agent" : "You"}</b>: {content}
          {messageId && (
            <a
              target="_blank"
              rel="noreferrer"
              href={`http://localhost:8080/workflow-runs/${messageId}`}
            >
              🪓
            </a>
          )}
        </p>
      ))}
 
      {openRequest && (
        <a
          target="_blank"
          rel="noreferrer"
          href={`http://localhost:8080/workflow-runs/${openRequest}`}
        >
          {status}
        </a>
      )}
    </div>
 
    <div className="Input">
      <textarea
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      ></textarea>
      <button onClick={() => sendMessage(message)}>Ask</button>
    </div>
  </div>
);

View Complete File on GitHub (opens in a new tab)

Note: CSS to make the frontend pretty can be found here (opens in a new tab)

In this step, we render the messages with the user's role and content. If the message has a messageId, we render a link to the Hatchet dashboard for the workflow run. We also render the current status of the workflow run if there is an openRequest.

And that's it! You now have a single-page React application that streams real-time progress updates from a Hatchet workflow. You can further customize the UI and add additional features as needed.