Data fetching with React Server Components in NextJS 13

Howard Lee
4 min readJun 9, 2023

Since React Server Components were released, I’ve been playing with them in the context of NextJS 13. I wanted to share my experience, particularly with regards to data fetching.

To follow along with this article, you need basic understanding of React, NextJs, and SQL.

First, create a NextJS app with App Router (not Page Router) and a local Postgres database. I didn’t use an ORM (I believe the best one for working with NextJs apps is Prisma) because I wanted to take the most stripped-down approach possible. After setting up the database, I created a table ```users``` and populated it with dummy data.

Instead of setting up a separate backend, I used NextJS to create internal API endpoints and connected them to my Postgres database.

// db.js
import {Pool} from 'pg';
// Create a pool instance and pass in our config, which we set in our env vars

let conn;

if (!conn) {
conn = new Pool({
user: "",
password: "",
host: "localhost",
port: 5432,
database: "",
});
}

export { conn };
//connecting API endpoints to db
//route.js
import { NextRequest, NextResponse } from 'next/server';
import { conn } from "@/app/db.js";

export const GET = async(req, params, res) => {
const {id} = params.params;

try{
const query = 'SELECT * FROM users WHERE id = $1 ORDER BY id ASC';//

const response = await conn.query(query, [id]);
return NextResponse.json(response.rows);
}catch (error) {
return NextResponse.status(400).json({ message: error.message });
}
}

Note that in App Router, components are RSCs by default and that you cannot have a page.js file (publicly available routes and UI) with a route.js file (server-side API endpoint) in the same directory.

To compare how the two different components fetch data, I had a client component and a RSC fetch from the same source. In this case, I wanted to view the user profile.

//client component for user profile
//page.js
'use client';
import React, {useState, useEffect} from 'react';

const UserProfile= ({params})=>{
const {id} = params;
const [user, setUser] = useState({});

useEffect(()=>{
const getUserInfo = async()=>{
const data = await fetch(`/user/${id}`);
const response = await data.json();
console.log(response[0]);
setUser(response[0])
};
getUserInfo();
},[id]);

return(
<>
<h1>Hello {user.name}</h1>

<h3>Edit user info</h3>
<form>
<input type="text" defaultValue={user.name} id='name' />
<input type="email" defaultValue={user.email} id='email'/>
</form>
</>
)
}

export default UserProfile;

Notice that the client component functions in the traditional React method: fetching the data from an API (in this case, an internal one) in ```useEffect``` and then setting it to state with ```useState```.

//server component
//page.js
import {NextResponse} from 'next/server';
import { conn } from "@/app/db.js";

const getUserProfile = async(id)=>{
try{
const query = 'SELECT * FROM users WHERE id = $1 ORDER BY id ASC';//
const response = await conn.query(query, [id]);
return response.rows;
}catch (error) {
throw new Error('failed to fetch data from db')
}
}

const UserProfile= async ({params})=>{
const {id} = params;
const userData = await getUserProfile(id);
const user = userData[0];
return(
<>
<h1>Hello {user.name} </h1>
<h3>contact me at {user.email}</h3>
</>
)
}

export default UserProfile;

In RSCs, we can query the database directly, but we do not have access to state and lifecycle methods, such as React Context or Hooks. We cannot interact with the data or the UI, nor do we have access to any browser APIs.

If my goal was to add interactivity to the page , we can pass the data as props into a client component . Bear in mind, though, that we cannot pass functions in server component as props (except as server actions which are still in the experimental phase).

import {NextResponse} from 'next/server';
import { conn } from "@/app/db.js";
import PLayout from './page_layout.js';

const getUserProfile = async(id)=>{
try{
const query = 'SELECT * FROM users WHERE id = $1 ORDER BY id ASC';//
const response = await conn.query(query, [id]);
return response.rows;
}catch (error) {
throw new Error('failed to fetch data from db')
}
}

const UserProfile= async ({params})=>{
const {id} = params;
const userData = await getUserProfile(id);
const user = userData[0];
return(
<>
<PLayout user={user} />
</>
)
}

export default UserProfile;
// page_layout.js
'use client';
import {useState} from 'react';

const PLayout = (props)=>{
const [user, setUser] = useState(props.user);

const handleChange =(e)=>{
const {id, value} = e.target;
setUser({...user,[id]: value} )
}

return(
<>
<h1>Hello {user.name}</h1>
<h3> contact me at {user.email}</h3>
<h3>Edit user info</h3>
<form onChange={handleChange}>
<input type="text" defaultValue={user.name} id='name' placeholder='name' />
<input type="email" defaultValue={user.email} id='email' placeholder='email'/>
</form>
</>
)
}

export default PLayout;

In summary: data fetching in RSCs work better for relatively static data that does not require user interactivity (such as a blog post).

--

--

Howard Lee

“Your sacred space is where you can find yourself over and over again.” — Joseph Campbell