React Server Components were previewed near the end of 2020. In this article, we'll explore what they are, the problems they solve, and how we can use them for direct database access.
Update (August 15th, 2023): Since this article has been published, the
react-prisma
package has been deprecated. You can query your database directly from a React Server Component using Prisma Client without thereact-prisma
package.At the time of writing, February 24th 2021, React Server Components are still being researched and far from being production-ready. The React core Team announced this feature to get initial feedback from the React community and in a spirit of transparency.
TL;DR
- React Server Components allow you to render components on the server and send them as data to your frontend. This data can be merged with the React client tree without losing state.
- You also ship significantly less JavaScript when using Server Components.
- Since Server Components live on the server we're going to see how to send queries to the database, skipping the API layer altogether. We'll do that using Prisma, an open-source ORM that provides an intuitive, type-safe API with clear workflows.
Here's the RFC and the full announcement talk:
This article summarizes the talk while making some changes to the official demo. Instead of sending raw SQL queries to the database, we'll be using Prisma, an open-source ORM. If you have watched the talk already, feel free to skip to the demo section of this article.
Using Prisma instead of plain SQL has several benefits:
- More intuitive querying (no SQL knowledge required)
- Better developer experience (e.g., through auto-completion)
- Safer database queries (e.g., prevents SQL injections)
- Easier to query relations
- Human-readable data model + generated (but customizable) SQL migration scripts
To learn more about Prisma and the different ways you can use it, check out the getting started guide.
Table Of Contents
- Good, fast, and cheap which two would you pick?
- Introducing Server Components
- Server Components demo
- Conclusion
Good, fast, and cheap which two would you pick?
When you're building a product, you'll often face this dilemma, do you create:
- A product that is good and fast but is expensive
- A product that is good and cheap but is slow
- A product that is cheap and fast but isn't good
When building frontends, we face a similar dilemma. We have three goals:
- Create a consistent user experience. (good)
- A fast experience where data loads quickly. (fast)
- Low maintenance: adding or removing components shouldn't be complicated and should require little work. (cheap)
Which two do we pick? Let's take a look at three different examples.
Say we're building an app like Spotify, here's the mockup of what it should look like:
This page contains information about a single artist, such as their top tracks, discography, and details. If we were to build this UI using React, we'd break it down into multiple components. Here's how we'd write it using React while using static data, where each component contains its data.
function ArtistPage({ artistId }) { return ( <ArtistDetails artistId={artistId}> <TopTracks artistId={artistId} /> <Discography artistId={artistId} /> </ArtistDetails> )}
Building a fast and consistent user experience
To add data fetching logic to an API, we'd need to fetch all data at once and pass it down to the different components. This way, we can achieve a consistent user experience by rendering all components at once. So we would end up with something like this:
function ArtistPage({ artistId }) { const data = fetchAllData() return ( <ArtistDetails details={data.details} artistId={artistId}> <TopTracks topTracks={data.topTracks} artistId={artistId} /> <Discography discography={data.discography} artistId={artistId} /> </ArtistDetails> )}
This approach is fast because we only need to make a single request to our API.
However, we find that the code is now harder to maintain. The reason being that the UI components are directly tightly coupled to the API response. So if we make a change in our UI, we need to update the API accordingly and vice-versa.
Otherwise, we may be passing unnecessary data that we're not using, or our UI won't render correctly.
So far, we have a good and fast user experience, but the code is harder to maintain.
Before adding the data fetching logic, we had an easy-to-maintain code where we could easily swap out components, so what happens if we try to make every component only fetch the data it needs?
Building a consistent user experience that is easy to maintain
If we add the data fetching logic inside each component, where each one fetches the data it needs, we'll end up with something like this.
function ArtistDetails({artistId, children }){ const details = fetchDetails(artistId); // ...}
function TopTracks = ({ artistId }) { const topTracks = fetchTopTracks(artistId); // ...}
function Discography({ artistId }) { const discography = fetchDiscography(artistId); // ...}
function ArtistPage ({ artistId }){ return ( <ArtistDetails artistId={artistId}> <TopTracks artistId={artistId} /> <Discography artistId={artistId} /> </ArtistDetails> )}
This approach is not fast because our parent component's children only start fetching data after the parent makes a request, receives a response, and renders.
So we end up having a waterfall of network requests, where network requests start one after the other, instead of all at once:
Prioritizing speed and ease of maintenance
What if we decide to decouple our components from the API by making separate requests and pass the data as props to our components? So in our Spotify app example, this is what our components will look like:
function ArtistPage({ artistId }) { // requests will not finish at the same time // nor at the same order const details = fetchDetails(artistId).data const topTracks = fetchTopTracks(artistId).data const discography = fetchDiscography(artistId).data
return ( <ArtistDetails details={details} artistId={artistId}> <TopTracks topTracks={topTracks} artistId={artistId} /> <Discography discography={discography} artistId={artistId} /> </ArtistDetails> )}
This pattern will result in inconsistent behavior because if all components start fetching data together, they don't necessarily finish simultaneously. That's because the data fetching process depends on the network connection, which can vary. So while now we have fast, easy-to-maintain code, we are sacrificing user experience.
So is it impossible to have all three? Not really.
Facebook faced this challenge and already came up with a solution using Relay and GraphQL fragments. Relay manages the fragments and only sends a single request, avoiding the waterfall of network requests issue.
Now while this may be a solution, not everyone can or wants to use GraphQL and relay. Perhaps you're working on a legacy codebase, or GraphQL is not the right tool for your use case.
So Facebook is now researching Server Components.
Introducing Server Components
In this section, we'll take a closer look at React Server Components, how they work and what their benefits are compared to traditional, client-side React components.
Rendering components on the server
When using React, all logic, data fetching, templating and routing are handled on the client.
However, with Server Components, components are rendered on the server. This allows our components to access all backend resources (i.e. database, filesystem, server, etc.). Also, since we now have access to the database, we can send queries directly from our components, skipping the API call step altogether.
After being rendered on the server, Server Components are sent to the browser in a JSON like format, which can the be merged with the client's component tree without losing state. (More details about the response format).
How is this different than server-side rendering (e.g. using Next.js)?
Server side rendered React is when we generate the HTML for a page when it is requested and send it to the client.
The user then has to wait for JavaScript to load so that the page can become interactive (this process is called hydration). This approach is useful for improving perceived performance and SEO.
Server Components are complementary to server side rendering but behave differently, the hydration step is faster since it uses their prepared output.
Shipping less code using Server Components
When building web apps using React, we sometimes run into situations where we need to format data coming from an API. Say for example our API returns a Date
object, meaning the date will look like this: 1614637596145
. A popular date formatting library is date-fns
. So what happens is we will include date-fns
in our JavaScript bundle, and the date-formatting code will be downloaded, parsed and and executed on the client.
With Server Components, we can use date-fns
to format the date object, render our component and then send it to the client. This way we don't need to include them in the client bundle. This is also why they're called "zero-bundle".
Server Components demo
While this project works, there are still many missing pieces that are still being researched, and the API most likely will change. The following code walkthrough isn't a tutorial but a display of what's possible today.
Here's a link to the repository we'll reference in this article: https://github.com/prisma/server-components-demo
.
To run the app locally run the following commands
git clone git@github.com:prisma/server-components-demo.gitcd react-server-components-demonpm installnpm start
The app will be running at http://localhost:4000 and this is what you'll see:
Project structure
When you clone the project you'll see the following directories:
server-components-demo/┣ notes/┣ prisma/┃ ┣ dev.db┃ ┗ schema.prisma┣ public/┣ scripts/┃ ┣ build.js┃ ┣ init_db.sh┃ ┗ seed.js┣ server/┃ ┣ api.server.js┃ ┗ package.json┗ src/ ┣ App.server.js ┣ Cache.client.js ┣ EditButton.client.js ┣ LocationContext.client.js ┣ Note.server.js ┣ NoteEditor.client.js ┣ NoteList.server.js ┣ NoteListSkeleton.js ┣ NotePreview.js ┣ NoteSkeleton.js ┣ Root.client.js ┣ SearchField.client.js ┣ SidebarNote.client.js ┣ SidebarNote.js ┣ Spinner.js ┣ TextWithMarkdown.js ┣ db.server.js ┗ index.client.js
The /notes
directory is where we save notes, in markdown format, when they're created on the frontend.
The /prisma
directory contains two files:
- A
dev.db
file, which is our SQLite database - A
schema.prisma
file, the main configuration file for our Prisma setup that's used to define the database connection and the database schema.
datasource db { provider = "sqlite" url = "file:./dev.db"}
generator client { provider = "prisma-client-js"}
model Note { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt title String? body String?}
The schema file is written in Prisma Schema Language (PSL). To get the best possible development experience, make sure you install our VSCode extension, which adds syntax highlighting, formatting, auto-completion, jump-to-definition, and linting for .prisma
files.
We specified that we're using SQLite and our dev.db
file location in the datasource
field.
Next, we're specifying that we want to generate Prisma Client based on our data models in the generator
field. Prisma Client is an auto-generated and type-safe query builder; we're going to see how it simplifies working with databases.
Finally, in this schema, we have a Note
model with the following attributes:
- An
id
of typeInt
, set as our primary key that auto-increments. - A
createdAt
, of typeDateTime
with a default value of the creation time of an entry. - An
updatedAt
, of typeDateTime
. - An optional
title
of typeString
. - An optional
body
of typeString
.
All fields in a model are required by default. We specify optional fields by adding a question mark (?) next to the type.
The /public
directory contains static assets, a style sheet and an index.html file.
The /scripts
directory contains scripts for setting up webpack, seeding the database and initializing it.
The /server
directory contains a api.server.js
file where we setup an Express API and initialize Prisma Client. If you're looking for a ready-to-run example of a REST API using Express with Prisma, we have JavaScript and TypeScript examples.
Building the API
This demo is a fullstack app with a REST API that has multiple endpoints for achieving CRUD operations. It's built using Express as the backend framework and Prisma to send queries to the database.
We're going to take a look at how the following functionalities are implemented:
- Creating a note.
- Getting all notes.
- Getting a single note by its id.
- Updating a note.
- Deleting a note.
When building REST APIs, Prisma Client can be used inside our route controllers to send databases queries. Since it is "only" responsible for sending queries to our database, it can be combined with any HTTP server library or web framework. Check out our examples repo to see how to use it with different technologies.
To create a note we created a /notes
endpoint that handles POST
requests. In the route controller we pass the body
and the title
of the note to the create()
function that's exposed by Prisma Client.
app.post( '/notes', handleErrors(async function(req, res) { const result = await prisma.note.create({ data: { body: req.body.body, title: req.body.title, }, })
// ... // return newly created note's id // in the response object sendResponse(req, res, result.id) }),)
To get all notes, we created a /notes
route and when we receive a GET
request we will call and await the findMany()
function to return all records inside the notes
table in our database.
app.get( '/notes', handleErrors(async function(_req, res) { // return all records const notes = await prisma.note.findMany() res.json(notes) }),)
A GET
request to /note/id
will return a single note when we pass its id
.
We get the note's id
from the request's parameters using req.param.id
and cast it to a number, since that's the type of the id
we defined in our Prisma schema.
We then use findUnique
which returns a single record by a unique identifier.
app.get( '/notes/:id', handleErrors(async function(req, res) { const note = await prisma.note.findUnique({ where: { id: Number(req.params.id), }, }) res.json(note) }),)
Finally, to update a note, we can send a PUT
requests to /notes/:id
and we access the note's id from the request parameters. We then pass it to the update()
function and pass the note's updates coming from the request's body.
app.put( '/notes/:id', handleErrors(async function(req, res) { const updatedId = Number(req.params.id) await prisma.note.update({ where: { id: updatedId, }, data: { title: req.body.title, body: req.body.body, }, }) // ... sendResponse(req, res, null) }),)
To delete a note, we send a DELETE
request to /notes/:id
. We then pass the note's id from the request parameters to the delete
function.
app.delete( '/notes/:id', handleErrors(async function(req, res) { await prisma.note.delete({ where: { id: Number(req.params.id), }, }) // ... sendResponse(req, res, null) }),)
Note that all Prisma Client operations are promise-based, that's why we need to use async/await (or promises) when sending database queries using Prisma Client.
A look at Server Components
The/src
directory contains our React components, you'll notice .client
and .server
extensions. These extensions is how React distinguishes between a component that will be rendered on the client or on the server. All .client
files are just regular React components, so let's take a look at Server Components.
Now to access backend resources from React Server Components, we need to use special wrappers called React IO libraries. These wrappers are needed to tell React how to deduplicate and cache data requests.
The React core team has already created wrappers for the fetch
API, accessing the file-system and for sending SQL queries to a PostgreSQL database.
These wrappers are not production-ready and most lilely will change.
So in the db.server.js
file, we're creating a new instance of Prisma Client. However notice that we're importing PrismaClient
from react-prisma
. This package allow us to use Prisma Client in a React Server Component.
//db.server.jsimport { PrismaClient } from 'react-prisma'
export const prisma = new PrismaClient()
In the NoteList.server.js
component, we're importing prisma
and the SidebarNote
component, which is a regular React component that receives a note object as a prop.
We're filtering the list of notes by making a query to the database using Prisma.
We're retrieving all records inside the notes
table, where the title
of a note, contains the serachText
. The searchText
is passed as a prop to the component.
// NoteList.server.jsimport { prisma } from './db.server'import SidebarNote from './SidebarNote'
export default function NoteList({ searchText }) { const notes = prisma.note.findMany({ where: { title: { contains: searchText ?? undefined, }, }, })
return notes.length > 0 ? ( <ul className="notes-list"> {notes.map(note => ( <li key={note.id}> <SidebarNote note={note} /> </li> ))} </ul> ) : ( <div className="notes-empty"> {searchText ? `Couldn't find any notes titled "${searchText}".` : 'No notes created yet!'}{' '} </div> )}
You'll notice that we don't need to await
prisma here, that's because React uses a different mechanism that retries rendering when the data is cached. So it's still asynchronous, but you don't need to use async/await.
Conclusion
You've now learned how to build an Express API using Prisma, consume it on the frontend and also use it in your React Server Components.
This new pattern is exciting because now we can have a good, fast user experience while having easy to maintain code. Because now, we have data fetching at the component-level.
We also end up having a faster user experience since less JavaScript is shipped to the browser.
Finally, React's virtual DOM now spans the entire application instead of just the client.
There are still many questions to be answered, and there are drawbacks, but it's exciting to see how the future of building Web apps using React might look like.