Enterprise Javascript
Next 14: The Rise of Full-stack Components and Heterogeneous Component Trees
Mo Sayed, Principal UI Developer
11 December 2023
Introduction
The recent Next.js Conference 2023 marked the release of Next 14 with a renewed emphasis on stability and refinement. While the conference didn't introduce any new APIs, it delved into solidifying the framework's core features, enhancing developer experience, and fostering a thriving community. Three key announcements were made:
1) Turbopack: Progress is being made in making Turbopack more stable with 5K tests passing with 53% faster local server start-up and 94% faster code updates. Once 100% tests are passing, it will be marked stable in an upcoming minor release. Webpack will still be supported for custom configurations and ecosystem plugins.
2) Partial Pre-rendering: This is a new compiler optimization for dynamic content in Next.js that aims to provide the best of both static and dynamic rendering. It does this by generating a static shell of your application's HTML, which includes placeholder elements for dynamic content. This static shell is then served immediately to the user, while the dynamic content is streamed in as part of the same HTTP request. This results in a faster initial loading experience for users, while still allowing you to deliver personalized and dynamic content.
3) Server Actions: This is the ability to execute server logic such as mutating data, sending an email etc. from within UI components, eliminating the need to create API endpoints (effectively in-lining API endpoints). Although server actions were introduced in v13.4, they were not considered stable. The latest v14 release has addressed these shortcomings and Server Actions are now considered mature enough for production.
The remainder of this article will focus on Server Actions and how together with React Server Components provide a powerful capability but one that comes with a cost.
An example of Server Actions is given below:
Here the server logic (annotated by ‘use server’) is invoked from the action attribute on the <form/> element. When the user clicks on ‘submit’, the ‘create’ function will be invoked on the server (via POST/). Previously, submit would have involved calling a REST endpoint. Server Actions allow you to cut out the middle man and insert server logic within your React component.
The announcement about Server Actions led to some interesting reactions. Firstly, many observers were concerned about the possibility of SQL injection when shown the following code snippet:
However as this article demonstrates, this is not a realistic outcome. SQL Template tags prevent the possibility of such malicious behaviour.
A second concern was that the example depicts a very simple scenario. Typical enterprise applications will have more complex and lengthy SQL queries that span multiple tables - a potential maintenance nightmare if cast in the same format. This example was obviously kept simple to better convey the main benefits of Server Actions without adding cognisance overhead. Nevertheless, it raises a number of questions such as how do Server Actions scale for Enterprise applications, typically characterized by a rich API economy? Where does API versioning fit in? What about Security and the risk of data leakage? These questions are discussed in the section below.
Full-stack Components and Heterogenous Component Trees
The combination of React Server Components and Server Actions will result in two interesting patterns:
Firstly, it gives rise to Full Stack Components - defined by Kent C Dodd as:
"complex UI components that are connected to a backend in the simplest and most powerful way you've ever seen"
Effectively these are components which are self-reliant on managing the state on the server and client. Secondly, it will lead to heterogeneous component trees composed of client and server components, suitably interleaved.
This combination of Server Components and Actions offers powerful capabilities but raises some thought-provoking questions and implications.
In the first place, traditionalists will be uncomfortable with the deviation from the standard practice of Separation of Concerns. Earlier versions of Next.js introduced the concepts of ISR, SSR, where server logic was co-located with UI code. However, in these versions, there was still a clear separation between Client and Server logic. The new features have blurred these lines - now your JSX can contain both client and server components.
Secondly, this co-location of client and server logic now introduces a more critical problem - that of exposing sensitive information. The intertwining of the two tiers makes it possible for data that needs to stay in the server leaking into the client tier. How can this be mitigated?
Additionally, Enterprise applications are typically characterized by rich APIs with well defined security models and versioning protocols. How will the React Server Components and Server Actions scale for these architectures?
Finally, from a development perspective, these changes introduce further complexity such as:
- Increased vigilance to prevent data leakage between the 2 tiers. PRs will need to contain a Security Audit check.
- Increased architectural considerations - Developers need to take more care in composing screens by identifying which components will be client / server (this will undoubtably increase context switching). Further, how do we incorporate existing APIs into the new framework?
- Increased likelihood of merge conflicts as front and back-end developers work on the same files.
As these features are relatively recent, it will be some time before best practices and patterns are elicited through experience. However, there are some things that can be done to mitigate the risk of data exposure as well as enabling development within Enterprise environments.
The ‘server-only’ package
Consider the following code:
import 'server-only'export async function fetchPost() {const res = await fetch('https://enterprise.com/posts', {headers: {authorization: process.env.MY_API_KEY,},})return res.json()}
Without the import, this utility function can be called on both the Client and Server tiers. The API key won’t be visible on the client as it’s missing the NEXT_PUBLIC prefix. However, mistakes do happen and it’s possible for our secret to be exposed. Using the ‘server-only’ import, any build that contains a client component which imports this function will result in a failure. The failure will be accompanied with a message explaining that this module can only be used within a server component. It’s worth noting a similar package ‘client-only’ exists and can be used to identify components that reference browser objects such as ‘window’.
The ‘taint’ package
The taint package in React is an experimental feature that helps prevent sensitive data from being leaked to the client-side. It works by marking certain values as "tainted" and preventing them from being passed to Client Components. The feature consists of the following api:
- taintUniqueValue - prevents individual values such as tokens, passwords from exposure
- taintObjectReference - protects object references such as User, Session objects.
The following example illustrates how to use the latter
import {experimental_taintObjectReference} from 'react';export async function getUser(id) {const user = await prisma.user.findUnique({where: {id: id,},});experimental_taintObjectReference('Error - Do not pass the entire user object to the client. ',user,);return user;}
Now if during development, the User object is passed to a client component, an error will be thrown warning of the data leakage. Similar behaviour is observed when using ‘taintUniqueValue’ to protect individual values e.g. API keys, tokens etc.
Note that this feature is only available in Next 14 by enabling the ‘taint’ flag.
module.exports = {experimental: {taint: true,},};
Next.js Security Guidance
As Server Actions and React Server Components are relatively recent developments, there is a lack of information available about best practices - especially in terms of Security and handling Rich API economies. However recently, Sebastian Markbåge from Vercel posted some guidance which can help in both areas. His post addresses the questions of how to mitigate data leaks and implicitly answers the questions raised above about handling Rich API economies and versioning.
In addition to the packages mentioned above regarding preventing data leaks, he identifies the following Data Access Models:
- HTTP APIs: In this approach, data access control is handled by API endpoints which serve fetch() calls from Server Components. This approach is recommended for existing large projects, particularly where there is a rich API registry. This will be ideal for companies who already have mature services and allows for existing security practices to be used. Further there is no longer the concern on how to version your APIs.
- Data Access Layer (DAL): This approach requires encapsulating all Data Access Logic into a separate JavaScript library. This will ensure consistent data access and will reduce the risk of authorization bugs. Additionally, it could lead to performance improvement via caching and in-memory data structures. The DAL is responsible for data checks, ensuring that only valid data is accessed by client logic. This approach is recommended for new projects.
- Component Level Data Access: In this approach, database queries are placed directly in Server Components as illustrated in the image above. This approach exposes more data to the client and increases the risk of security vulnerabilities and as such is only recommended for prototyping and learning
In addition to the obvious benefits of mitigating data leakage, the first two approaches will also benefit from reducing the surface area of code that needs to be audited for Security risks. Lastly, the article highly recommends following the Principle of Least Information. Always ensure that you only expose the required information and no more e.g. a user’s email and not the full User object.
On a final note, it will be interesting to see how the combination of Server Components and Actions will impact State Management libraries and other tools such as React Query. Version 5 of this library was recently released and provides improved support for Server Components. Its docs mention:
"Server Components and streaming are still fairly new concepts and we are still figuring out how React Query fits in and what improvements we can make to the API."
The decision to utilise this library alongside Server Components will vary from application to application. Having the capability to prefetch your data on the Server, and then to hydrate your cache on the client with support for Suspense boundaries as offered by React Query will provide flexibility for tackling a wider range of problems. The new Next.js features are bleeding edge and will alter the way data is fetched - these changes will most certainly affect development practices and libraries in the coming years.
Summary
The Next.js Conference 2023 announced the release of Next 14, focusing on stability and refinement. Key updates include improvements in Turbopack, a new compiler optimization called Partial Pre-rendering, and the maturing of Server Actions for production use.
The maturing of Server Actions coupled with React Server Components raises some questions around development of Enterprise Applications. Utilisation of these features introduces potential security risks, impacts development complexity and requires careful consideration and design. Despite the features being cutting edge, some guidance is provided in mitigating strategies such as the 'server-only' and 'taint' packages, and data access models to prevent data leaks and handle rich API economies. Given that these features are relatively recent, it will take some time for best practices to emerge.
Insights to your inbox
Enter your email below, and we'll notify you when we publish a new blog or Thought Leadership article.