February 24, 2021

Database Access in React Server Components

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.

React Server Components are still being researched at the time of writing 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?

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:

  1. Create a consistent user experience. (good)
  2. A fast experience where data loads quickly. (fast)
  3. 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:

Spotify user interfaceSpotify user interface

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:

Diagram of a network waterfallDiagram of a network waterfall

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).

Traditional React components vs. Server ComponentsTraditional React components vs. Server Components

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.git
cd react-server-components-demo
npm install
npm start

The app will be running at http://localhost:4000 and this is what you'll see:

Server Components demo screenshotServer Components demo screenshot

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 type Int, set as our primary key that auto-increments.
  • A createdAt, of type DateTime with a default value of the creation time of an entry.
  • An updatedAt, of type DateTime.
  • An optional title of type String.
  • An optional body of type String.

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.js
import { 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.js
import { 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.

Join the discussion

Follow @prisma on Twitter

Don’t miss the next post!

Sign up for the Prisma newsletter