Streaming Real-Time Results with React, OpenAI, and Server Sent Events (SSE): A Step-by-Step Guide

Written by Sebastian on February 12, 2023 · 15 min read

tutorial

In this guide, we’ll explore how to stream real-time results in a React web application using OpenAI’s GPT-3 API and Server Sent Events (SSE). With the rise of AI and machine learning, OpenAI’s GPT-3 has become one of the most powerful and versatile tools for natural language processing and text generation. By combining this with the simplicity and performance of React, and the real-time capabilities of SSE, you can create a truly outstanding web application.

In this step-by-step guide, we’ll take you through the process of setting up your React environment, integrating OpenAI’s GPT-3 API, and using SSE to stream real-time results. Get ready to enhance your web application development skills and create a truly dynamic and engaging user experience.

What Are We Going To Build

Let’s take a quick glimpse on the web application we’re going to build in this step-by-step guide …

Image

The application we’re building will be able to deliver real-time text responses retrieved from OpenAI’s GPT-3 language model.

In order to build this web application we’ll be making use of the following technologies:

Step 1: Obtain The OpenAI API Key

In order to be able to access the OpenAI API from within your Python script we need to use an API key.

To retrieve your OpenAI API key you need to create a user account at https://openai.com/ and access the API Keys section in the OpenAI dashboard to create a new API key.

Image

This key is secret and must not shared with anybody else. We’ll need to use this key later of when implementing the Python script to access OpenAI`s API.

Step 2: Create The React Project Using Yarn And Vite

Let’s now create a new React project with vite. Type in the following command to start the creation process on the command line:

$ yarn create vite

You’ll then be prompted to enter a new project name next:

Image

Type in react-openai-sse and hit return to continue. Vite will then provide you with a list of JavaScript frameworks you can choose from. Use the arrow key to select the entry “React” and hit return again:

Image

Once you’ve selected “React” you’re asked to specify the variant of the React project. You have four options to choose from:

Image

So you can decide to create a React project which is using just plain JavaScript or which is supporting Typescript as well. In addition you can choose to use SWC which is a new Rust-based bundler, which claims to be a lot of faster then JS-based bundlers. For the example of this article we’re choosing option “JavaScript +SWC”.

Once selected the project setup is completed:

Image

You can now enter the new project directory (with the name of the project you’ve entered:

$ cd react-openai-sse

Next you need to execute the yarn command:

$ yarn

This makes sure that all the needed dependencies of the project are being downloaded and installed:

Image

Step 3: Installing Dependencies

The next step is to install the needed dependencies: Chakra UI and sse.js. Let’s start with Chakra UI. In order to install the UI library you need to execute the following command inside the project folder:

$ yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion

Second make sure to install the sse.js libraray into our project by using the following command:

$ yarn add sse@https://github.com/mpetazzoni/sse.js

Step 4: Cleaning Up The Project

In file src/App.js you can already find a lot of default code. Let’s clean this code up to only contain an empty implementation for App component which we’re using as a starting point:

function App() {
  return (
  );
}

Step 5: Implementing The User Interface (UI)

In order to activate Chakra UI for our React application we need to embed the main application component into <ChakraProvider></ChakraProvider>. There the ChakraProvider component must first be imported from @chakra-ui/react. Adapt the code in src/main.jsx to the following:

ReactDOM.createRoot(document.getElementById('root')).render(
	<React.StrictMode>
		<ChakraProvider>
			<App />
		</ChakraProvider>
	</React.StrictMode>
)

Next open src/App.jsx. First let’s add an import statement on top of the file which imports all Chakra UI components which we will be using the following:

Now start implementing the user interface by adding the Chakra UI’s Flex component to the return statement of App component in the following way:

return (
	<Flex
		width={'100vw'}
		height={'100vh'}
		alignContent={'center'}
		justifyContent={'center'}
		bgGradient="linear(to-b, orange.100, purple.300)"
	></Flex>
)

This code creates a Flex component using the Chakra UI library in React. A Flex component is used to create flexible and responsive layout containers.

The code sets the width and height of the Flex container to 100vw and 100vh, respectively. This means that the container will take up the full width and height of the viewport.

The alignContent property is set to center, which aligns the content along the cross axis (vertically) in the center of the container. The justifyContent property is also set to center, which aligns the content along the main axis (horizontally) in the center of the container.

Finally, the bgGradient property is set to linear(to-b, orange.100, purple.300). This sets the background gradient of the container to a linear gradient that starts with the color orange.100 and transitions to purple.300.

So, in short, the code creates a full-page container that has a centered layout and a background gradient that transitions from orange to purple.

Inside of the Flex component we do need to add a few more Chakra UI elements like you can see in the following listing:

return (
	<Flex
		width={'100vw'}
		height={'100vh'}
		alignContent={'center'}
		justifyContent={'center'}
		bgGradient="linear(to-b, orange.100, purple.300)"
	>
		<Box maxW="2xl" m="0 auto" p="20px">
			<Heading
				as="h1"
				textAlign="center"
				fontSize="5xl"
				mt="100px"
				bgGradient="linear(to-l, #7928CA, #FF0080)"
				bgClip="text"
			>
				React & OpenAI
			</Heading>
			<Heading as="h2" textAlign="center" fontSize="3xl" mt="20px">
				With Server Sent Events (SSE)
			</Heading>
			<Text fontSize="xl" textAlign="center" mt="30px">
				This is a React sample web application making use of OpenAI's GPT-3 API to perform prompt
				completions. Results are received using Server Sent Events (SSE) in real-time.
			</Text>
			<Textarea
				value={prompt}
				onChange={handlePromptChange}
				placeholder="Insert your prompt here ..."
				mt="30px"
				size="lg"
			/>
			<Button
				isLoading={isLoading}
				loadingText="Loading..."
				colorScheme="teal"
				size="lg"
				mt="30px"
				onClick={handleSubmitPromptBtnClicked}
			>
				Submit Prompt
			</Button>
			<Button colorScheme="teal" size="lg" mt="30px" ml="20px" onClick={handleClearBtnClicked}>
				Clear
			</Button>
			{result != '' && (
				<Box maxW="2xl" m="0 auto">
					<Heading as="h5" textAlign="left" fontSize="lg" mt="40px">
						Result:
					</Heading>
					<Text fontSize="lg" textAlign="left" mt="20px">
						{result}
					</Text>
				</Box>
			)}
		</Box>
	</Flex>
)

This code creates a Box component using the Chakra UI library in React. A Box component is used to create a layout container.

The Box component has the properties maxW set to "2xl", m set to "0 auto", and p set to "20px". This sets the maximum width of the container to 2xl, centers the container horizontally and vertically with a 0 margin on the top and bottom and an auto margin on the left and right, and adds a padding of 20px around the container’s contents.

The code then creates several Heading components, which are used to display headings in different sizes, with fontSize properties set to 5xl, 3xl, and lg, respectively. The first Heading component has a background gradient set using the bgGradient property, and the text is clipped to the gradient using the bgClip property.

There are also two Text components used to display text with a fontSize of xl and lg, respectively. The text is aligned to the center of the container using the textAlign property.

A Textarea component is used to allow the user to input text, with the value property bound to the prompt state and the onChange property set to the handlePromptChange function. The placeholder property provides a hint to the user of what to input.

Two Button components are used for user interaction, with one for submitting the prompt and another for clearing the result. The onClick properties of the buttons are bound to the handleSubmitPromptBtnClicked and handleClearBtnClicked functions, respectively. The isLoading property of the submit button is used to display a loading state while the result is being processed.

Finally, the code conditionally displays a Box component with a heading and a text component to show the result of the API call. The result is displayed only if it is not an empty string.

In short, this code creates a centered layout container with headings, text, a text input area, and buttons for user interaction. The result of an API call is displayed if it is not an empty string.

Step 6: Implementing OpenAI API Access By Using Server Sent Events

In the next step we need to write the code which is needed to implement the communication logic with the OpenAI API. From our React application we’d like to send the text which was entered by the user to the HTTP POST Completion’s endpoint which is available at

https://api.openai.com/v1/completions

From this endpoint we expect to receive the text completion for the given prompt. We’ll use the sse.js library to receive the response by listing to Server-Sent Events. This will allow us to receive the text response as it gets generated word by word.

Let’s start by adding some more import statements to App.jsx first:

This code imports three hooks from the React library: useState, useRef, and useEffect. These hooks allow you to add state and side effects to your functional components in React.

Additionally, the code imports an object SSE from a module called sse. SSE (Server-Sent Events) is a standard for real-time communication between a client and a server. With Server-Sent Events, the server can send data to a client without the client having to request it, which is useful for building real-time applications like chat rooms, stock tickers, and other types of applications where updates need to be pushed from the server to the client.

Next we need to make the OpenAI API key available in our application. In our Vite-based React project we’ll be using the .env.local file in the root folder of our project to contain the definition of an environment variable which is holding the API key:

VITE_OPENAI_API_KEY=[INSERT YOUR OPENAI API KEY HERE]

In App.jsx we are then able to retrieve the key from the environment variable by the following line of code:

const API_KEY = import.meta.env.VITE_OPENAI_API_KEY

Let’s continue with defining our state and ref variables with the App component function:

function App() {
  let [prompt, setPrompt] = useState("");
  let [isLoading, setIsLoading] = useState(false);
  let [result, setResult] = useState("");

  const resultRef = useRef();

  useEffect(() => {
    resultRef.current = result;
  }, [result]);
...

This code defines a functional React component called App. The component uses several React hooks to manage its state and side effects.

The useState hook is used three times to manage the state of the component. The first time, it creates a state variable prompt and its updater function setPrompt. The initial value of prompt is an empty string.

The second time, it creates a state variable isLoading and its updater function setIsLoading. The initial value of isLoading is false.

The third time, it creates a state variable result and its updater function setResult. The initial value of result is an empty string.

The useRef hook is used to create a ref called resultRef. Refs are a way to access the properties of DOM elements and can be used to maintain a reference to a value across multiple renders of a component. In this case, resultRef.current is assigned the value of result every time result changes, effectively making it a way to persist the value of result across renders.

The useEffect hook is used to run a side effect whenever the result value changes. The side effect is to update resultRef.current with the new value of result, allowing it to persist across renders.

Finally we’re ready to implement to logic which is needed to interact with the OpenAI Completion endpoint and receive the answer as Server-Send Events by using the sse.js library. This logic is implemented in the handleSubmitPromptBtnClicked function which is invoked everytime the user clicks on the Submit Prompt button:

let handleSubmitPromptBtnClicked = async () => {
	if (prompt !== '') {
		setIsLoading(true)
		setResult('')
		let url = 'https://api.openai.com/v1/completions'
		let data = {
			model: 'text-davinci-003',
			prompt: prompt,
			temperature: 0.75,
			top_p: 0.95,
			max_tokens: 100,
			stream: true,
			n: 1
		}

		let source = new SSE(url, {
			headers: {
				'Content-Type': 'application/json',
				Authorization: `Bearer ${API_KEY}`
			},
			method: 'POST',
			payload: JSON.stringify(data)
		})

		source.addEventListener('message', (e) => {
			if (e.data != '[DONE]') {
				let payload = JSON.parse(e.data)
				let text = payload.choices[0].text
				if (text != '\n') {
					console.log('Text: ' + text)
					resultRef.current = resultRef.current + text
					console.log('ResultRef.current: ' + resultRef.current)
					setResult(resultRef.current)
				}
			} else {
				source.close()
			}
		})

		source.addEventListener('readystatechange', (e) => {
			if (e.readyState >= 2) {
				setIsLoading(false)
			}
		})

		source.stream()
	} else {
		alert('Please insert a prompt!')
	}
}

The function is an asynchronous arrow function, which allows it to use the await keyword to wait for a promise to resolve.

The function first checks if the prompt value is not equal to an empty string. If it is not, the function continues to execute. If it is, an alert message is displayed to the user saying “Please insert a prompt!“.

The function then sets the state value of isLoading to true and the state value of result to an empty string.

Next, it constructs a URL and an object data that contains the data to be sent to the API endpoint.

It then creates a new instance of the SSE class, which is the sse.js library’s class for working with Server-Sent Events. The class is instantiated with the url, headers, method, and payload options.

The function adds two event listeners to the source instance. The first listens for the message event and updates the value of resultRef.current and the state value of result with the response text received from the API. The second listens for the readystatechange event and sets the state value of isLoading to false when the ready state of the source changes to 2 or higher.

Finally, it calls the stream method on the source instance to initiate the API request.

In short, this function makes an API request to the OpenAI API with the given prompt and updates the state values and refs accordingly with the response data. As our UI components are bound to the respective state variables, the output on the website is updated everytime a new chunk of data is received from the OpenAI GPT-3 language model.

Step 7: Implementing Clear Input Functionality

Finally we need to implement the click event handler function for the Clear button as well:

let handleClearBtnClicked = () => {
	setPrompt('')
	setResult('')
}

This code defines a JavaScript function handleClearBtnClicked. The function is an arrow function, which is a shorthand for writing anonymous functions in JavaScript. The function is executed when the clear button is clicked.

The function does two things when it is executed:

  1. It calls the setPrompt function and passes an empty string as an argument. setPrompt is the state updater function that updates the state value of the prompt in your React component.
  2. It calls the setResult function and passes an empty string as an argument. setResult is the state updater function that updates the state value of the result in your React component.

The purpose of this function is to clear the values of both the prompt and the result in the state of your React component and therewith is resetting the inputs and outputs in the user interface.

Step 8: Testing The Application

Finally we’re ready to test the application by starting the development web server by executing the following command in the project folder:

$ yarn dev

The response on the command line informs you about the port on which the web server is running:

Image

When accessing the local URL in the browser you should be able to see the output of the React application in the browser:

Image

You can start entering your prompt in the text input field right away:

Image

Clicking on button “Submit Prompt” will initiate the OpenAI request and you’ll be able to see the output once the first chunk of data is received from the API:

Image

The button will indicate a loading state as long ad the final chunk of data has been received and the answer is fully available

Image

Conclusion

This guide has shown you how to bring together the power of OpenAI’s GPT-3 API, the simplicity and performance of React, and the real-time capabilities of Server Sent Events (SSE) to create a truly outstanding web application. By following the step-by-step instructions, you now have the knowledge and tools necessary to stream real-time results in your own React web application. Whether you’re a beginner or an experienced developer, this guide has given you the foundation to take your web application development skills to the next level. So what are you waiting for? Start exploring and integrating these amazing technologies into your own projects today!