Polymorphic React Components for Both the Client and the Server

This ad is not shown to multipass and full ticket holders
React Summit US
React Summit US 2025
November 18 - 21, 2025
New York, US & Online
The biggest React conference in the US
Learn More
In partnership with Focus Reactive
Upcoming event
React Summit US 2025
React Summit US 2025
November 18 - 21, 2025. New York, US & Online
Learn more
Bookmark
Rate this content

Explore Server Components through the lens of reusable UI Component, where everything "depends" on the individual requirements of the use-case and individual application needs. Instead of fighting over 'server' vs 'client' - let's have the best of both worlds.

This talk has been presented at React Summit US 2024, check out the latest edition of this React Conference.

FAQ

BakaUI is a design system framework currently being built by Kirill.

Rewriting the UI with Server Components was a bad idea because client components are still necessary for interactivity and some features.

A polymorphic data table is a single component that can be used both on the client and as a server component.

Kirill developed a server data table, which sends only HTML and interacts with the server, and a client component, which is interactive and works with browser APIs.

Key considerations include avoiding code duplication, allowing customization by consumers, and accommodating changing requirements.

The composition pattern allows mixing and matching server and client components within the rendering tree of a data table.

By not rendering their own children and separating their client logic into a client shell, components can be rendered as either client or server components.

Kirill is building the next generation of the KendoUI React library full-time.

React Server Components promise a smaller bundle size, better performance, and improved data fetching.

View components assemble the actual slot, prepare its children, and ensure that components can be either client or server without affecting their children.

Kiril Peyanski
Kiril Peyanski
10 min
22 Nov, 2024

Comments

Sign in or register to post your comment.
Video Summary and Transcription
Hi, my name is Kirill and I have a little obsession with UI components. Let's talk React, specifically React 19 Server Components. I will show you how to build a polymorphic data table using server components. We explore mixing server and client components and applying the composition pattern. We discuss polymorphic components and separating client logic to render custom components without breaking client functionality. The component can be used in different environments, morphing into server or client components accordingly. This talk focuses on building a polymorphic component with minimal bundle size and access to both server and client APIs.

1. Introduction to React 19 Server Components

Short description:

Hi, my name is Kirill and I have a little obsession with UI components. Let's talk React, more specifically, the new edition of React 19, Server Components. When Server Components initially came out, they promised a smaller bundle size, better performance, and improved data fetching. Today, I want to show you how I built what I call a polymorphic data table, a single component which can be used on the client and as a server component.

Hi, my name is Kirill and I have a little obsession with UI components. In fact, I'm doing this full-time at Progress, building the next generation of the CanDo React library. During my free time, I love exploring the design system space, and currently I'm building a design system framework which I'm calling BackerUI.

But enough about me. Let's talk React, and more specifically, the new edition of React 19, Server Components, which if you ask me, should have been called simply React Components. But anyway, when Server Components initially came out, they promised a smaller bundle size, better performance, and improved data fetching, which led me to believe that I should rewrite all my user interface to use Server Components. This turned out to be a really bad idea, even Lee Robinson had to tweet that client components are actually fine. At some point, I even had a server button. It looked exactly like a regular button, but it was mostly useless because it didn't have a click event. I ended up keeping my client components.

But along the way, I realized I had a client and a server variant of some of my components. On paper, a data table could greatly benefit from being a server component, but I still needed to provide interactivity for implementing more dynamic dashboards or e-commerce shopping carts. Today, I want to show you how I built what I call a polymorphic data table, a single component which can be used to put on the client and as a server component. But before that, let's make sure that we are all visualizing the same thing when we think data table. A data table usually displays tabular data. It can have a header, a body, multiple rows with cells inside which shows formatted data. Cool. Now we're on the same page. But I still ended up with two really similar looking UI components.

One server data table, which sent only HTML to the client, had access to my file system and even a database instance. And one client component, which was interactive. I could edit, sort, and filter the data without a trip back to the server and had access to the browser APIs and the DOM. Usually, if I was building my personal blog posts, it would be perfectly fine to have two similar looking UI components with different purposes. However, if you are like me and are building a reusable UI library, being an open source one or something for other teams in your organization, there are a couple of things you should consider. First is code duplication. You probably do not want to maintain two separate code bases, which do almost the same thing, rendering a couple of rows with cells inside. Second is how consumers of your library would further modify to meet their requirements. A lot of applications have individual needs which may require a mix of server and client code, customizing all parts of your component along the way. And lastly, requirements change. They always do.

Read also

2. Mixing Server and Client Components

Short description:

Someone starting with the server component should be able to easily add interactivity later on without having to replace the whole component. Let's explore how the rendering tree of a data table would look like and see if we can apply the composition pattern there. Most of the rendering tree would end up not being able to use server components at all.

Someone starting with the server component should be able to easily add interactivity later on without having to replace the whole component. A specific pattern has emerged to help us mix and match server and client components. You have probably seen this diagram, which we refer to when speaking about the composition pattern.

Everything looks good on paper, but it is often applied on an application level. When we zoom into the component level, UI libraries are usually going full client mode, which diminishes the benefits of using server components. Let's explore for a bit how the rendering tree of a data table would look like and see if we can apply the composition pattern there. If you have tried implementing a data table before, you would quickly come to the realization that you need client code almost anywhere in the tree. Being for accessibility or adding a simple row selection feature, most of the rendering tree would end up not being able to use server components at all.

This is problematic because consumers of your data table might still want to keep some of the rendering on the server. But from the rules of server components, we know that while server components can render client components, the opposite is not true. There is, however, a way to achieve this by passing everything through the children pod prop, but we are going to explore this in a minute. Now, back to our data table.

3. Polymorphic Components and Separating Client Logic

Short description:

To render a markdown cell without affecting the client code, we can use a pattern called a polymorphic component. By following some basic rules, such as not rendering their own children and separating client logic, we can render custom components across the whole component tree without breaking client functionality. The rendering tree should consist of agnostic nodes that can be rendered as either client or server components. The client parts of a component are extracted into a client shell, while an intermediate view component assembles the actual slot and prepares its children. By extracting all client logic into a separate component and adding a client suffix to its name, we can work around the rendering of the component without affecting the tree and consume the client context in other components.

My favorite example is an API table which shows documentation for UI component or software of any kind. To make everything readable and beautiful, we are sometimes rendering markdown with code blocks inside, but markdown libraries are chunky and often end up in your client bundle. It's even worse if you want to syntax highlight thing for the code blocks.

What I want to be able to do is render a markdown cell without affecting the client code. I know there are some other ways to achieve this, but the most straightforward implementation for me would be something like this as passing a specialized cell which does all the job without negatively affecting the client bundle.

Now, let's deep dive into how we can achieve this by using a pattern which I have called a polymorphic component, a component which can be can render any of its nodes both on the client or the server. To achieve this, we must simply follow some basic rules. First, components should not be rendering their own children. And second, their client logic should be separated.

With those two simple rules, we would be able to render custom components across the whole component tree without breaking client functionality and still be able to morph into a fully client component for application which does not support or require server components. If we apply those rules correctly, the rendering tree should look something like this. Each node in the tree is agnostic, meaning it could be rendered both as a client or as a server component. Additionally, the client parts of a component are extracted into a client shell where they are not affecting the rendering of the original component.

Now let's go over the first rule of not being responsible for your own children in the context of UI components of course. If previously our root data table components was directly rendering the role, we are now introducing an intermediate component. I still haven't found a good name for those, but for now I'm calling them view components. The idea of a view component is to assemble the actual slot which can be customized, in our case the role, and prepare its children so that if the role needs to be either a client or a server component, its children, the cells, would not be affected by this decision, allowing them to be anything according to the application requirements.

Referring back to our use case with the markdown cell, the rendering pattern would allow us to render a server component in a leaf regardless of any other customization which we have made up in the tree. Now to our second rule. Eventually, you or your product manager would want to add a simple role selection feature and wouldn't care much about the performance implication if we bring the whole component to the client.

When I was first playing with server components, I thought that I could simply add anything from React to a component and it would work on the server instead of the client. But if you try to add the state to a component, which might be rendered on the server, like our agnostic parts of the data table, you would quickly end up with an error. The same goes to attaching DOM events, because how else would you know how to change the state? So, what we are seeing on the screen now does not work.

In order to make it work, we must extract all client logic into a separate component. Again, the best name I have come up with is adding a client at the end of the component's name. This allows us to work around the rendering of the component without affecting the tree directly and only providing the client context. This allows us to consume this context from the client's part of our other components. Again, only adding client-specific logic.

In our use case, that would be the role's client part, which must only add a click event and append a class name if the client state indicates that the role is selected. Zooming out to see the full picture, our port of mobile components look something like this, rendering from left to right and its client logic being extracted into a separate component with the use client directive.

4. Using the Component in Different Environments

Short description:

When using this component in a regular client application, its agnostic parts morph into client components. However, in a React server component environment like Next.js or a Wacom application, the agnostic component morphs into server components, leaving JavaScript on the server and sending HTML. The client parts of the component are shipped separately. This concludes the journey of building a polymorphic component with minimal bundle size and access to both server and client APIs. Follow me on Twitter or X and check out my work on polymorphic components for Gendo React and the open-source Baka.UI design system framework.

Now, let's try to guess what happens if we use this component in a regular client application, for example, a Create React app or a Vite application. The component is not working correctly, since its agnostic parts are morphing into client components. Our components will still not work, but that's up to the frameworks underneath, not our component.

However, if we consume our component in a React server component environment on the right, like Next.js or a Wacom application, we could expect that our agnostic component to morph into server ones, leaving their JavaScript on the server and only sending or streaming, if you prefer, the HTML which they have produced. The client parts of our component is shipped in a separate bundle as we have separated it from the component itself.

With this, we conclude our journey of building a single polymorphic component which can be used as client component or as a server component or anything in between, actually, with minimal bundle size, interactivity, and access to both server and client APIs from its corresponding parts. Thanks for watching. If you have enjoyed this talk, you can follow me up on Twitter or X, if you prefer. If you would like to check my work, I'm currently building polymorphic components for Gendo React, and also feel free to check out my open-source design system framework, Baka.UI. Bye!

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Scaling Up with Remix and Micro Frontends
Remix Conf Europe 2022Remix Conf Europe 2022
23 min
Scaling Up with Remix and Micro Frontends
Top Content
This talk discusses the usage of Microfrontends in Remix and introduces the Tiny Frontend library. Kazoo, a used car buying platform, follows a domain-driven design approach and encountered issues with granular slicing. Tiny Frontend aims to solve the slicing problem and promotes type safety and compatibility of shared dependencies. The speaker demonstrates how Tiny Frontend works with server-side rendering and how Remix can consume and update components without redeploying the app. The talk also explores the usage of micro frontends and the future support for Webpack Module Federation in Remix.
Understanding React’s Fiber Architecture
React Advanced 2022React Advanced 2022
29 min
Understanding React’s Fiber Architecture
Top Content
This Talk explores React's internal jargon, specifically fiber, which is an internal unit of work for rendering and committing. Fibers facilitate efficient updates to elements and play a crucial role in the reconciliation process. The work loop, complete work, and commit phase are essential steps in the rendering process. Understanding React's internals can help with optimizing code and pull request reviews. React 18 introduces the work loop sync and async functions for concurrent features and prioritization. Fiber brings benefits like async rendering and the ability to discard work-in-progress trees, improving user experience.
Full Stack Components
Remix Conf Europe 2022Remix Conf Europe 2022
37 min
Full Stack Components
Top Content
RemixConf EU discussed full stack components and their benefits, such as marrying the backend and UI in the same file. The talk demonstrated the implementation of a combo box with search functionality using Remix and the Downshift library. It also highlighted the ease of creating resource routes in Remix and the importance of code organization and maintainability in full stack components. The speaker expressed gratitude towards the audience and discussed the future of Remix, including its acquisition by Shopify and the potential for collaboration with Hydrogen.
Thinking Like an Architect
Node Congress 2025Node Congress 2025
31 min
Thinking Like an Architect
Top Content
In modern software development, architecture is more than just selecting the right tech stack; it involves decision-making, trade-offs, and considering the context of the business and organization. Understanding the problem space and focusing on users' needs are essential. Architectural flexibility is key, adapting the level of granularity and choosing between different approaches. Holistic thinking, long-term vision, and domain understanding are crucial for making better decisions. Effective communication, inclusion, and documentation are core skills for architects. Democratizing communication, prioritizing value, and embracing adaptive architectures are key to success.
The Eternal Sunshine of the Zero Build Pipeline
React Finland 2021React Finland 2021
36 min
The Eternal Sunshine of the Zero Build Pipeline
For many years, we have migrated all our devtools to Node.js for the sake of simplicity: a common language (JS/TS), a large ecosystem (NPM), and a powerful engine. In the meantime, we moved a lot of computation tasks to the client-side thanks to PWA and JavaScript Hegemony.
So we made Webapps for years, developing with awesome reactive frameworks and bundling a lot of dependencies. We progressively moved from our simplicity to complex apps toolchains. We've become the new Java-like ecosystem. It sucks.
It's 2021, we've got a lot of new technologies to sustain our Users eXperience. It's time to have a break and rethink our tools rather than going faster and faster in the same direction. It's time to redesign the Developer eXperience. It's time for a bundle-free dev environment. It's time to embrace a new frontend building philosophy, still with our lovely JavaScript.
Introducing Snowpack, Vite, Astro, and other Bare Modules tools concepts!
Composition vs Configuration: How to Build Flexible, Resilient and Future-proof Components
React Summit 2022React Summit 2022
17 min
Composition vs Configuration: How to Build Flexible, Resilient and Future-proof Components
Top Content
Today's Talk discusses building flexible, resilient, and future-proof React components using composition and configuration approaches. The composition approach allows for flexibility without excessive conditional logic by using multiple components and passing props. The context API can be used for variant styling, allowing for appropriate styling and class specification. Adding variants and icons is made easy by consuming the variant context. The composition and configuration approaches can be combined for the best of both worlds.

Workshops on related topic

AI on Demand: Serverless AI
DevOps.js Conf 2024DevOps.js Conf 2024
163 min
AI on Demand: Serverless AI
Top Content
Featured WorkshopFree
Nathan Disidore
Nathan Disidore
In this workshop, we discuss the merits of serverless architecture and how it can be applied to the AI space. We'll explore options around building serverless RAG applications for a more lambda-esque approach to AI. Next, we'll get hands on and build a sample CRUD app that allows you to store information and query it using an LLM with Workers AI, Vectorize, D1, and Cloudflare Workers.
High-performance Next.js
React Summit 2022React Summit 2022
50 min
High-performance Next.js
Workshop
Michele Riva
Michele Riva
Next.js is a compelling framework that makes many tasks effortless by providing many out-of-the-box solutions. But as soon as our app needs to scale, it is essential to maintain high performance without compromising maintenance and server costs. In this workshop, we will see how to analyze Next.js performances, resources usage, how to scale it, and how to make the right decisions while writing the application architecture.