Published on May 16, 2024 (2 months ago)

React 19 lets you write impossible components

Darius Cepulis
By Darius Cepulis13 min readEngineering

React 19 is here. And with it? Complex and flexible new features. Incredible optimizations. And a whole new way of thinking about your app.

But when building a marketing site, are React 19’s features over-engineered? Or is it all worth the effort?

For the last 18 months, these features have slowly been rolling out in React Canary and the Next.js App Router. And, for the last 18 months, we’ve been using them in our docs and our marketing site. So our team’s got some opinions.

Let’s talk about React 19’s new features, and how they make the future of React development bright, even if you’re just writing a marketing site.

LinkServer Components

So I mentioned “a whole new way of thinking about your app”. The biggest part of that new paradigm is Server Components. You might’ve heard of them – they’ve been an absolute earthquake in the React community. If you’re familiar, skip to this next subheader. Otherwise, here’s a quick refresher:

LinkWhat are Server Components? (The fast version)

Back in the day, when we wrote React, we would ship that React code to the client, and that code would make HTML (again, on the client). Now, we have two types of components. Client components work the way React used to — on the client. And new Server Components run on the server and spit out HTML on the server1. In other words, we can choose which of our code runs on the client and which of our code can stay on the server. Usually, stuff that needs to be interactive (i.e., respond to user input and change over time) gets shipped to the client. Usually, stuff that can run once and never have to run again, like an <h1> or a <p>, stays on the server.

Server Components can do more than just let you pick where your code runs. Server Components also let you fetch data directly within your component. Previously, you’d have to run framework-specific functions like getServerSideProps or build some sort of framework GraphQL data layer. Now? Nice and simple. Does your comment component need to talk to your comment database? Put your comment database code right there, in your comment component.

There’s one more magic trick you can pull with Server Component data fetching. Let’s say your comment database is really slow. Previously, your whole page would have to wait for that database call to complete. Now, if you wrap your comment component in Suspense, it’ll stream to your client when it’s ready. In other words, your user can begin reading your page immediately, and when your comment database finally does its thing, that comment component can pop in later.

LinkLiving with Server Components after the honeymoon

Server Components leave an incredible first impression. When we first implemented them, we immediately shaved off 20%-90% of our pages’ bundle sizes. That’s wild. But it makes sense — our marketing site and documentation site are largely static content, like paragraph tags and headers and highlighted code blocks and a markdown loader. These components don’t need to respond to user interaction. That means they can stay on the server. This alone makes Server Components worth considering for marketing websites.

That being said, Server Components also demand that you climb a steep learning curve. After the honeymoon, the reality is that it’s hard to keep everything in your head at once. It’s hard to avoid the footguns of mixing Client and Server Components. The learning curve is so steep that I had to write a 4,000-word blog post just to get it all straight in my head. You should check out that post if you’re climbing the curve too.

Since we’ve already climbed the curve, though, I want to focus on what Server Components do for us now. How have Server Components changed how we write our websites?

LinkThe future of Server Components is bright

Now that we’ve migrated our marketing and documentation websites to Server Components, we can write impossible new components.

Pretty early on, we took advantage of Server Components’ new data-fetching superpowers. For example: our whole docs page would have to wait for the changelog sidebar to get its data from our database, even though that sidebar usually begins off-screen.

before
export default async function Page() { const changelogData = await getChangelogData() /* all of this has to wait for changelogData to load */ return ( <Layout> <Sidebar> {/* other sidebar stuff */} <Changelog data={changelogData} /> </Sidebar> {/* other page stuff */} </Layout> ) }

Now, instead of making the whole page wait for the changelog data fetching, we moved that fetching into the component and wrapped that component in Suspense.

after
import { Suspense } from 'react'; /* We've moved data fetching into the changlog compoment */ async function ChangelogWithDataFetching() { const changelogData = await getChangelogData() return <Changelog data={changelogData} /> } /* ...and wrapped that component in Suspense. The user can see the page immediately, while the changelog component loads */ export default function Page() { return ( <Layout> <Sidebar> {/* other sidebar stuff */} <Suspense fallback={<ChangelogPlaceholder />}> <ChangelogWithDataFetching /> </Suspense> </Sidebar> {/* other page stuff */} </Layout> ) }

This optimization was just the start of our Server Component journey. As we kept building, we realized that there were components, like our Pricing component on our marketing pages, that required their own document in the database. Before, if we added the pricing component to the page, we’d also have to add the pricing component’s document retrieval to the page or to an API endpoint, somewhere else. Now, since the pricing component fetches its own data, we can just plop in the component and move along. Server-side data fetching used to be separate from components; now it’s in the same place.

For some, it’s controversial that React encourages us to put our logic, our markup, and our styling all in the same file. For some, that single-file approach violates the “separation of concerns” principle so many of us in programming hold dear. However, as Cristiano Rastelli argues with their iconic diagram, React components are still “separating concerns”, but by context, not by technology. React components are at their best when they let you see everything about them in one place. Adding server-side data to our components extends this, and adds another layer to Rastelli’s diagram:

Once that way of thinking became ingrained, we started thinking of components we would never have written before. Take, for example, our new VideoGlossaryHoverCard component. We frequently have to talk about video terms that no normal person should have to know off the top of their head, like HDCP and LL-HLS. We wanted to add definitions to those terms when you hover over them. Here’s what the finished component looks like:

And here’s what it looks like under the hood. First, I wrote a VideoGlossaryHoverCard Server Component: given a glossary link, query the database for that glossary term and display it in a hover card. (Isn’t it sweet that we can do all that in one place?). Next, I added a conditional to our Link component: if you detect a video glossary link, show the video glossary hover card. (And the hover card is wrapped in Suspense to make sure this component doesn’t slow the main experience down.)

components/Link.jsx
import 'server-only'; import { Suspense } from 'react'; import NextLink from 'next/link'; import VideoGlossaryHoverCard from './VideoGlossaryHoverCard'; export default function Link({ href, ...rest }) { const linkComponent = <BaseNextLink href={href} {...rest} ref={ref} />; if (href?.includes('/video-glossary/')) { return ( <Suspense fallback={linkComponent}> <VideoGlossaryHoverCard href={href}>{linkComponent}</VideoGlossaryHoverCard> </Suspense> ); } return linkComponent; }

Let me underline why this component would’ve been a catastrophic pain before. If we wanted to perform this work on the client, we would’ve had to write a whole API endpoint and then query that endpoint in a useEffect in VideoGlossaryHoverCard and do all the boilerplate involved with that. Something we’ve all done and we’re all sick of. Or, imagine doing this work on the server at the page level! We’d have to go through our markup and detect every single video glossary term on the page, and then write a query against our database for all those terms, and then prop-drill those terms down to the link… and… ugh. No.

One simple const glossaryTerm = await getGlossaryTerm(href). One simple component. All with minimal boilerplate, all in one place. I love it. Once you start thinking in Server Components, you can never go back.

LinkActions

In short, if you call an async function in a special way2, you’ll get three things for free. First (and most importantly) you get access to an isPending state. No more managing setIsPending yourself. Second, Actions are hooked into React’s native Error Boundary feature. This means that when things go wrong, you can easily show an error state to your user. And finally, Actions work with a new hook called useOptimistic which lets you show your user the result of the action before it completes. For example, if you click a 🩷 button, you can show a cute 💖 animation before the API has responded successfully.

So much boilerplate melts away when React handles this all for you. If you want to see, check out React’s great examples on their blog. But Actions are just the tip of the boilerplate-melting iceberg. What we find really changes things for marketing sites are Server Actions.

LinkBringing it all together with Server Actions

Server Actions add one last layer of cake to the boilerplate-melting iceberg. (I acknowledge: these analogies are out of control.) Server Actions melt the boilerplate of writing API endpoints, too.

Let’s imagine you want to build a little “contact support” box on your website. Well, now you’ve got to write an endpoint to handle comments on the backend. And then you need to write a little form for your website with a textarea; when that form submits, you’ll need to POST that data to your new endpoint and manage all that pending and optimistic UI and error handling.

But wait! Actions manage pending and optimistic UI and error handling… which leaves just that annoying API endpoint. This is where Server Actions come in. Instead of writing an API endpoint, you write a Server Action. And then, instead of interacting with that API endpoint by POSTing with fetch, you just call your Server Action like a function.

Let’s take a look at an example of a Server Action in action. By putting "use server" at the top of a file, you’re telling your bundler “Hey, turn this into a Server Action. Turn this into an endpoint to which my client can POST.”

actions.js
"use server" // the "use server" directive tells React to make this its own bundle // that can be called as a Server Action. // under the hood, React turns this into its own API endpoint for you. export async function saveContactFormAction(formData) { const name = formData.get('name') const text = formData.get('text') try { await saveToDatabase(name, text) return { message: 'Text saved successfully' } } catch (error) { return { message: error.message } } }

That’s not too dissimilar from an API endpoint… but once we get to the front end, that’s where magic begins. We can just import that Server Action and pass it to a form’s action prop. And then, to get that easy isPending state I mentioned, we tap into React’s new useActionState hook.

page.jsx
"use client" import { useActionState } from "react" import saveContactFormAction from "./actions" const initialState = { message: '' } export default function ContactForm() { const [response, formAction, isPending] = useActionState(saveContactForm) return ( <form action={formAction}> <label>Name: <input type="text" name="name" required /></label> <label>Text: <textarea name="text" required /></label> <button type="submit" disabled={isPending}> {isPending ? "Submitting..." : "Submit"} </button> <p aria-live="polite">{response?.message}</p> </form> }

No setIsLoading. No fetch('/api/contact', { method: 'POST' }). Just calling a function and getting everything else for free. This is just a simple example; for so much more, definitely check out this Next.js doc.

LinkThe future of Server Actions seems bright

We’ve been working with Server Components a lot longer than Server Actions. Over a year into Server Components, we started seeing cool new patterns like that video glossary hover box that we talked about earlier. I think we’re starting to see that happen for Server Actions as well.

In the example above, we respond to the action with an object that contains a success and message. You know what else is an object, though? Server Components are just objects. What if we responded to our Server Action with a Server Component? For example, here’s a cool pagination feature we added to our changelog last week:

This new pagination function on our changelog was extremely easy to write with Server Actions

And here’s the outrageously simple action behind that:

actions.jsx
'use server' import { getPosts } from './data' import Posts from './posts' export async function getPostPageAction(formData) { const page = formData.get('page') const posts = await getPosts(page) return <Posts posts={posts} /> }

We then added a button to the bottom of our changelog. When the action responds, the button is replaced with new posts from the Server Action:

load-post-page-button.jsx
'use client'; import { useActionState } from 'react'; import Button from 'components/button'; import { getPostPageAction } from './actions'; const defaultComponent = null; export default function LoadPostPageButton({ page }) { const [component, formAction, isPending] = useActionState(getPostPageAction, defaultComponent); return component || ( <form action={formAction}> <input type="hidden" name="page" value={page} /> <Button cta disabled={isPending} type="submit"> {isPending ? 'Loading...' : 'Load more'} </Button> </form> ); }

And that’s how easy it was to write this pagination interaction. No client-side data management. Barely any code shipped to the client. No API endpoint. No pending state management. Just a great new user experience.

LinkThe Future of React is bright

There’s so much more coming to React 19 that’s going to improve our lives as web developers. React is adding component-level support for document metadata, stylesheets, async script tags, and preloading other resources. Earlier, we talked about how React is at its best when it lets you view everything about a component in the same place. These new features let you collocate even more relevant functionality for your component.

There’s some higher-level features I’m really excited about, too! Fewer and better hydration errors, a nicer developer experience for Context and forwarding refs, and don’t even get me started on first-class web component support – we’re so jazzed, Dylan wrote a whole blog post about it.

I could go on, but, oh, look at the word count. Maybe it’s time to start wrapping up instead.

There’s a concept in technology market research called the Gartner hype cycle. It shows how a new technology entering a market explodes as we try to apply it to every problem under the sun. Then, as we grow more familiar with that technology, first we’re disappointed that it couldn’t solve all of the world’s problems. Then, as the technology becomes an old friend, we start seeing the technology for what it is: a really good tool for a specific set of problems.

Especially with Server Components, the honeymoon was so bright, with its smaller bundles and fashionable new data loading. Then, we sloshed through the trough of disillusionment as we realized that the developer experience of Server Components is challenging.

Now, though? We’re learning how to think about and how to teach these new concepts. And we’re seeing their true potential. So much of our time as web developers is just spent sending data to the client, and sending data from the client back to the server. So much of our job is boilerplate surrounding these two tasks. React 19 sees this, and melts the boilerplate. React 19 is a great tool that makes communicating between the server and the client easier, so we can be more productive.

  1. Let’s gloss over server-side rendering and static site generation, which do some work on the server, but still ship all that code to the client for hydration, and require you to learn framework-specific methods. Let's also gloss over the fact that client components still get SSR'd and hydrated in some frameworks like Next.js.
  2. The technical way to say "in a special way" would be "if you call an async function as a transition." In React, one way to call transitions using the useTransition hook. Another way is with React's new Server Actions functionality, which we'll discuss in about two paragraphs, so stay tuned.

Written By

Darius Cepulis

Pretends he knows more about coffee than he does. Happier when he's outside. Thinks the web is pretty neat.

Leave your wallet where it is

No credit card required to get started.