AI SDK RSC Overview

React Server Components (RSC) gives you the ability to write UI that can be rendered on the server and streamed to the client. This way, language model generations and UI updates can be done from the server, making it easier to work with them in the same place and keep them in sync.

In this section you will learn about how language models can go beyond text by rendering React components as part of their generations, how they can get increasingly difficult to build when they're rendered on the client as your application grows, and how React Server Components can help you stream them from the server instead.

Rendering User Interfaces with Language Models

Language models generate text, so at first it may seem like you would only need to render text in your application.

app/actions.tsx
const text = generateText({
model: openai('gpt-3.5-turbo'),
system: 'you are a friendly assistant'
prompt: 'what is the weather in SF?'
tools: {
getWeather: {
description: 'Get the weather for a location',
parameters: z.object({
city: z.string().describe('The city to get the weather for'),
unit: z
.enum(['C', 'F'])
.describe('The unit to display the temperature in')
}),
execute: async ({ city, unit }) => {
const weather = getWeather({ city, unit })
return `It is currently ${weather.value}°${unit} and ${weather.description} in ${city}!`
}
}
}
})

In the above example, the language model calls the getWeather function and returns the weather information as text. However, instead of returning text, if you return a JSON object that represents the weather information, you can use it to render a React component instead.

app/action.ts
const text = generateText({
model: openai('gpt-3.5-turbo'),
system: 'you are a friendly assistant'
prompt: 'what is the weather in SF?'
tools: {
getWeather: {
description: 'Get the weather for a location',
parameters: z.object({
city: z.string().describe('The city to get the weather for'),
unit: z
.enum(['C', 'F'])
.describe('The unit to display the temperature in')
}),
execute: async ({ city, unit }) => {
const weather = getWeather({ city, unit })
const { temperature, unit, description, forecast } = weather
return {
temperature,
unit,
description,
forecast
}
}
}
}
})

Now you can use the object returned by the getWeather function to conditionally render a React component <WeatherCard/> that displays the weather information by passing the object as props.

app/page.tsx
return (
<div>
{messages.map(message => {
if (message.role === 'function') {
const { name, content } = message
const { temperature, unit, description, forecast } = content;
return (
<WeatherCard
weather={{
temperature: 47,
unit: 'F',
description: 'sunny'
forecast,
}}
/>
)
}
})}
</div>
)

Here's a little preview of what that might look like.

What is the weather in SF?
getWeather("San Francisco")
Thursday, March 7
47°
sunny
7am
48°
8am
50°
9am
52°
10am
54°
11am
56°
12pm
58°
1pm
60°
Thanks!

Weather

An example of an assistant that renders the weather information in a streamed component.

Rendering interfaces as part of language model generations can help you elevate the user experience of your application so people can interact with language models beyond text.

They also make it easier for you to interpret sequential function calls that take place in multiple steps and help precisely identify and debug exact locations of incorrect reasoning performed by the model.

Rendering Multiple User Interfaces

To recap, an application has to go through the following steps to render user interfaces as part of model generations:

  1. The user sends a message to the language model.
  2. The language model generates a response that includes a function call.
  3. The function call returns a JSON object that represents the user interface.
  4. The response is sent to the client
  5. The client receives the response and checks if the latest message was a function call.
  6. If it was a function call, the client renders the user interface based on the JSON object returned by the function call.

Most applications have multiple functions that are called by the language model, and each function can return a different user interface.

For example, a function that searches for courses can return a list of courses, while a function that searches for people can return a list of people. As this list grows, the complexity of your application will grow as well and it can become increasingly difficult to manage these user interfaces.

app/page.tsx
{
message.role === 'function' ? (
message.name === 'api-search-course' ? (
<Courses courses={message.content} />
) : message.name === 'api-search-profile' ? (
<People people={message.content} />
) : message.name === 'api-meetings' ? (
<Meetings meetings={message.content} />
) : message.name === 'api-search-building' ? (
<Buildings buildings={message.content} />
) : message.name === 'api-events' ? (
<Events events={message.content} />
) : message.name === 'api-meals' ? (
<Meals meals={message.content} />
) : null
) : (
<div>{message.content}</div>
);
}

Rendering User Interfaces on the Server

In order to solve the problem of managing all your React components on the client side, React Server Components (RSC) allow you to render React components on the server and stream them to the client.

This prevents you from conditionally rendering user interfaces on the client based on the data returned by the language model, and instead allows you to directly stream them from the server during a model generation.

app/action.ts
import { createStreamableUI } from 'ai/rsc'
const uiStream = createStreamableUI();
const text = generateText({
model: openai('gpt-3.5-turbo'),
system: 'you are a friendly assistant'
prompt: 'what is the weather in SF?'
tools: {
getWeather: {
description: 'Get the weather for a location',
parameters: z.object({
city: z.string().describe('The city to get the weather for'),
unit: z
.enum(['C', 'F'])
.describe('The unit to display the temperature in')
}),
execute: async ({ city, unit }) => {
const weather = getWeather({ city, unit })
const { temperature, unit, description, forecast } = weather
uiStream.done(
<WeatherCard
weather={{
temperature: 47,
unit: 'F',
description: 'sunny'
forecast,
}}
/>
)
}
}
}
})
return {
display: uiStream.value
}

The createStreamableUI function belongs to the ai/rsc module and helps you create a stream that can send React components to the client.

So on the server, you are directly rendering the <WeatherCard/> component with the props passed to it, and then streaming it to the client. As a result, on the client side, you only need to render the UI that is streamed from the server.

app/page.tsx
return (
<div>
{messages.map(message => (
<div>{message.display}</div>
))}
</div>
);

Now the steps involved are simplified as:

  1. The user sends a message to the language model.
  2. The language model generates a response that includes a function call.
  3. The function call renders a React component along with relevant props that represent the user interface.
  4. The response is streamed to the client and rendered directly.

Note: You can also render text on the server and stream it to the client using React Server Components. This way, all operations from language model generation to UI rendering can be done on the server, while the client only needs to render the UI that is streamed from the server.

In the following sections, you will learn about how the ai/rsc module provides you with tools to confidently integrate generative user interfaces into your application.