Course
React Server Components
Next.js Mastery: From Fundamentals to Full-Stack
Unlock the power of Next.js with this comprehensive course! Starting with the basics, you’ll learn essential skills such as routing, data fetching, and styling. Progress through practical projects, including building your own React Notes app, to gain hands-on experience. Dive into the source code to understand the inner workings and core principles of Next.js. Perfect for developers with a basic knowledge of React and Node.js, this course ensures you’ll be ready to create high-performance full-stack applications efficiently. Join us and master Next.js, backed by industry experts and a supportive learning community.
React Server Components
The Evolution of Next.js
Next.js version 13 marked a significant milestone in the framework's evolution with the introduction of the App Router. This new routing solution is built on the foundation of React Server Components, representing a revolutionary update that fully embraces and implements the concept that the React team has been advocating for.
The introduction of React Server Components in Next.js has led to a fundamental shift in how components are categorized and rendered. Components are now distinctly classified as either client-side or server-side. However, recognizing that many developers may not be familiar with React Server Components, it's crucial to start by exploring the background of this technology and differentiate it from the often misunderstood concept of Server-Side Rendering (SSR).
A Brief History
On December 21, 2020, the React team unveiled React Server Components through a comprehensive introductory article. This introduction was accompanied by an in-depth presentation and demo, delivered by Dan Abramov and Lauren Tan, both engineers from the React team. Their presentation delved into the background, rationale, and usage of React Server Components.
To fully grasp Next.js's rendering methods, it's essential to understand React Server Components. Let's review the key points from this pivotal presentation.
The Three Pillars of Application Development
Dan Abramov introduced three critical aspects of application development that developers constantly strive to balance:
- Good user experience
- Ease of maintenance
- High performance
However, achieving all three simultaneously often proves challenging. To illustrate this, let's consider a real-world example: Spotify's website.
The Spotify Example: Balancing UX, Maintainability, and Performance
Imagine an artist introduction page on Spotify, primarily consisting of two main areas: a Top Tracks section and a Discography section. If we were to implement such a page using React, we might start with a simple component structure:
function ArtistPage({ id }) { return ( <ArtistDetails id={id}> <TopTracks id={id} /> <Discography id={id} /> </ArtistDetails> );}
This structure appears concise and clean. However, when we introduce data fetching, the complexity increases significantly:
function ArtistPage({ id }) { const [details, setDetails] = useState(null); const [tracks, setTracks] = useState(null); const [discography, setDiscography] = useState(null);
useEffect(() => { // Fetch all data in one request fetchArtistData(id).then(({ details, tracks, discography }) => { setDetails(details); setTracks(tracks); setDiscography(discography); }); }, [id]);
if (!details || !tracks || !discography) return <Loading />;
return ( <ArtistDetails details={details}> <TopTracks tracks={tracks} /> <Discography discography={discography} /> </ArtistDetails> );}
While this approach solves the problem with a single request, it introduces maintainability issues. For instance:
- If a UI component is removed in future iterations, but its corresponding data isn't removed from the API, it creates redundant data fetching.
- If a new field is added to the API and used in one component, but forgotten to be passed in another component that references it, this could lead to errors.
The Maintainability vs. Performance Tradeoff
To improve maintainability, we might revert to a simpler structure where each component is responsible for its own data fetching:
function ArtistPage({ id }) { return ( <ArtistDetails id={id}> <TopTracks id={id} /> <Discography id={id} /> </ArtistDetails> );}
function ArtistDetails({ id, children }) { const [details, setDetails] = useState(null); useEffect(() => { fetchArtistDetails(id).then(setDetails); }, [id]); if (!details) return <Loading />; return <div>{/* render details */}{children}</div>;}
// Similar implementations for TopTracks and Discography
However, this approach comes at the cost of performance. What could have been solved with one request is now split into three separate requests. This raises the question: Is it impossible to have the best of both worlds?
The Root Cause: Client-Side Requests
The core issue lies in the fact that these data requests are being made on the client side. Multiple HTTP requests from the client inevitably slow down the application. This problem is exacerbated if the requests are serial (for example, if the TopTracks and Discography components need to wait for the ArtistDetails component's data to return before sending requests with the id data).
Enter React Server Components
To address this challenge, React introduced Server Components. The key idea is to move the data request part to the server side, with the server directly returning components with data to the client.
In a traditional setup with only Client Components, a React tree structure might look like this:
[Root] ├─ [ArtistPage] │ ├─ [ArtistDetails] │ ├─ [TopTracks] │ └─ [Discography]
After introducing React Server Components, the React tree transforms into:
[Root (Server)] ├─ [ArtistPage (Server)] │ ├─ [ArtistDetails (Server)] │ ├─ [TopTracks (Client)] │ └─ [Discography (Client)]
Here, server components (marked as "Server") are rendered on the server side. React will render this into a tree containing basic HTML tags and placeholders for client components. The structure might resemble:
[Root (HTML)] ├─ [div] │ ├─ [h1] Artist Name │ ├─ [p] Artist Bio │ ├─ [ClientComponent Placeholder: TopTracks] │ └─ [ClientComponent Placeholder: Discography]
Client components are replaced with special placeholders in this tree, as their data and structure are only known when rendered on the client side.
The Server-to-Client Journey
This server-rendered tree can't be sent directly to the client. Instead, React serializes it. After the client receives this serialized data, it reconstructs the React tree, then fills in the placeholders with actual client components, rendering the final result.
Benefits of React Server Components
- Reduced Bundle Size: The code for server components isn't bundled into the client-side code, leading to smaller JavaScript bundles.
- Direct Backend Access: In React Server Components, you can directly access backend resources without additional API layers.
- Improved Performance: By moving data fetching to the server, we reduce the number of client-side requests and improve initial page load times.
Limitations of Server Components
While powerful, server components do have some limitations:
- They can't use client-side hooks like
useEffect
- They can't handle client-side events directly
- They can't maintain client-side state
Summary
In his presentation, Dan Abramov mentioned that the React team would collaborate with partners from the Next.js team to make React Server Components available to everyone. True to this promise, Next.js implemented React Server Components in version 13, two years after the initial announcement.
This implementation in Next.js v13 represents a significant step forward in web development, offering developers powerful new tools to build fast, maintainable, and user-friendly web applications.