When my team and I started experimenting with React Server Components (RSC) while building Hydrogen, our React-based framework for building custom storefronts, I was incredibly excited. Not only for the impact this would have on Hydrogen, and the future of ecommerce experience (goodbye large bundle sizes, hello improved buying experiences!), but also for the selfish reason that many of us developers have when encountering new tech: this is going to be fun.
And, indeed, it was… but it was also pretty challenging. RSC is a paradigm shift and, personally, it took some getting used to. I started out building way too many client components and very few server components. My client components were larger than they needed to be and contained logic in them that really had no business existing on the client. Eventually, after months of trial and error and refactoring, it eventually clicked for me. I found it (dare I say it?) easy to build server and client components!
In this post, I’m going to dive into the patterns and best practices for RSC that both myself and my team learned while building Hydrogen. My goal is to increase your understanding of how to approach writing components in an RSC application and cut down your trial-and-error time. Let’s go!
Default to Shared Components
When you need to build a component from scratch in a RSC application, start out with a shared component. Shared components’ entire functionality can execute in both server and client contexts without any issues. They’re a natural middle ground between client and server components and a great starting point for development.
Starting in the middle helps you ask the right questions that lead you to build the right type of component. You’ll have to ask yourself: “Can this bit of code run only on the client?” and, similarly, “Should this bit of code execute on the client?” The next section identifies some of the questions that you should ask.
In our experience, the worst approach you can take in a RSC application is to default to always building client components. While this will get you up and running quickly, your application ends up with a larger than necessary bundle size, containing too many client components that are better suited as server components.
Pivot to a Client Component in Rare Cases
The majority of the components in your RSC application should be server components, so you’ll need to analyze the use case carefully when determining if a client component is even necessary.
In our experience, there are very specific use cases in which a shared component should be pivoted to a client component. Generally, it’s not necessary to convert the entire component into a client component, only the logic necessary for the client needs to be extracted out into a client component. These use cases include
- incorporating client side interactivity
- using lifecycle rendering logic (for example,
- making use of a third-party library that doesn’t support RSC
- using browser APIs that aren’t supported on the server.
An important note on this: don’t just blindly convert your whole shared component into a client component. Rather, intentionally extract just the specific functionality you need into a client component. This helps keep your client component and bundle size as small as possible. I’ll show you some examples at the end of this post.
Pivot to a Server Component as Often as Possible
If the component doesn’t include any of the client component use cases, then it should be pivoted to a server component if it’s one of the following use cases:
- The component includes code that shouldn’t be exposed on the client, like proprietary business logic and secrets.
- The component won’t be used by a client component.
- The code never executes on the client (to the best of your knowledge).
- The code needs to access the filesystem or databases (which aren’t available on the client).
- The code fetches data from the storefront API (in Hydrogen-specific cases).
If the component is used by a client component, dig into the use cases and implementation. It’s likely you could pass the component through to the client component as a child instead of having the client component import it and use it directly. This eliminates the need to convert your component into a client component, since client components can use server components when they’re passed into them as children.
Explore Some Examples
These are a lot of things to keep in mind, so let’s try out some examples with the Hydrogen starter template.
Our first example is a component that allows buyers to sign up to my online store’s newsletter. It appears in the footer on every page, and it looks like this:
We’ll start with a shared component called
In this component, we have two pieces of client interactivity (input field and submit button) that indicates that this component, as currently written, can’t be a shared component.
Instead of fully converting this into a client component, we’re going to extract just the client functionality into a separate
And then update the
NewsletterSignup component to use this client component:
It would be tempting to stop here and keep the NewsletterSignup component as a shared component. However, I know for a fact that I want this component to only be used in the footer of my online store, and my footer component is a server component. There’s no need for this to be a shared component and be part of the client bundle, so we can safely change this to a server component by simply renaming it to
And that’s it! You can take a look at the final Newsletter sign-up product on Stackblitz.
For the next example, let’s add a product FAQ section to product pages. The content here is static and will be the same for each product in my online store. The interaction from the buyer can expand or collapse the content. It looks like this:
Let’s start with a shared
Next, we’ll add it to our product page. The
ProductDetails.client component is used for the main content of this page, so it’s tempting to turn the
ProductFAQs into a client component so that the
ProductDetails component can use it directly. However, we can avoid this by passing the
ProductFAQs through to the
And then update the
ProductDetails component to use the children:
Next, we want to add the client interactivity to the
ProductFAQs component. Again, it would be tempting to convert the
ProductFAQ component from a shared component into a client component, but that isn't necessary. The interactivity is only for expanding and collapsing the FAQ content—the content itself is hardcoded and doesn’t need to be part of the client bundle. What we’ll do instead is extract the client interactivity into an exclusively client component,
We’ll update the
ProductFAQs component to use the Accordion:
At this point, there’s no reason for the
ProductFAQs component to remain a shared component. All the client interactivity is extracted out and, similar to the
NewsletterSignup component, I know this component will never be used by a client component. All that’s left now is to:
- rename the file from
- update the import statement in
- add some nice styling to it via Tailwind.
You can view the final Product FAQ code on Stackblitz.
React Server Components are a paradigm shift, and writing a component for an RSC application can take some getting used to. Keep the following in mind while you’re building:
- Start out with a shared component.
- Extract functionality into a client component in specific cases.
- Pivot to a server component if the code never needs to or never should execute on the client.
Learn More About Hydrogen
- Building Blocks of High Performance Hydrogen-powered Storefronts
- Rapid Development with Hydrogen: Building a Product Page
Wherever you are, your next journey starts here! If building systems from the ground up to solve real-world problems interests you, our Engineering blog has stories about other challenges we have encountered. Intrigued? Visit our Engineering career page to find out about our open positions and learn about Digital by Design.