Video Summary and Transcription
Kiko shares the process of building UI components for both client and server sides, highlighting performance benefits and challenges of using server components. Building a reusable UI library with server and client components, aiming for similarity and adaptability. Adding interactivity to a data table in a Next.js application by incorporating client components for reactivity and customization. Building customization by incorporating a custom cell, resolving errors between client and server components, and optimizing syntax highlighting and markdown processing. Achieving composition pattern at the component level by separating client and server parts, maintaining an agnostic component part, and optimizing rendering for client-specific functionalities. Achieving client-server performance optimization by keeping rendering tree agnostic, enabling separate client component extraction, and promoting interactivity within components.
1. Building UI Components for Client and Server
Kiko shares the process of building UI components for both client and server sides, highlighting performance benefits and challenges of using server components.
Hey, folks. Do you remember server components? Do you use them or do you even like them? Because I do. In fact, I like all kinds of UI components, being client or server components. I'm Kiko, and I'm building UI components for a living, add progress for developers around the world. And today I want to share with you how I built UI components for both the client and the server.
So once again, I'm Kiko. I'm building UI components for both the client and the server. So when server components were initially announced, they brought all kinds of performance improvements like smaller bundle size and improved data fetching. So naturally, the developer inside of me really wanted to rebuild all of my user interface to be server components. A little bit of a side note, if you ask me, they should have been simply called React components without the server button. Whatever.
So by rebuilding all of my user interface to server components, I have built even the smallest server button, which was really not so that useful. But other components like a data table that could actually benefit from server components as a feature was an interesting experiment. So what I ended up with by building a server data table was two different components. On the one side, we have a completely static server data table. It has close to zero JavaScript shipped to the client and it also had access to my file system and even my database. On the other hand, I had a client data table, something that was interactive, something that I could virtualize by having access to the actual DOM. So those two components ended up in two different folders, which is probably fine if you're building your personal blog post.
2. Balancing Server and Client Component Capabilities
Building a reusable UI library with server and client components, aiming for similarity and adaptability. Challenging to have both server and client capabilities through React composition pattern.
But for me, I'm building a reusable UI library. I'm shipping those components to NPM and to teams across my organization. So I naturally end up with a lot of code duplication. I have a server component and a client component, which I had to maintain. But in the end of the day, they were kind of similar. They both were rendering roles and those roles were rendering cells. So I wanted to keep them somewhat similar. And also the requirements might change.
If you're starting from a server, at some point, you may need to add interactivity. And if you're starting from the client, at some point, you may want to outsource a specific piece of work to the server. So naturally, what I'm going to challenge today is can't we have both? This is somewhat solved on an application level in React through the composition pattern, which follows two really simple rules. Basically, server components can render client components, but the opposite is not true. But there is a little bit of a workaround this. So you can slot server children inside client components without the client components knowing about them.
When you reach a point in your application where you consume a third-party library, it's usually a black box from then on. And UI libraries naturally defaults to client components. You have no control over them, what is happening in their rendering tree. Today, I want to try to build a data table together with you to see how we can have the best of both worlds. I have my Chrome open. I have my cursor open. So let's start building from scratch. I'm going to import a data table and I have prepared some data in advance. Usually how I build a data table is through those two main properties, which are the rows and the columns.
3. Building Interactivity in Next.js Data Table
Adding interactivity to a data table in a Next.js application by incorporating client components for reactivity and customization.
is through those two main properties, which is the rows and the columns. And let's start to build the table from there. It's accepting the new properties and it would be rendering what? It would be rendering a table head and a table body and inside the table body, we're going to be rendering some rows going over to the rows again, except columns and a data and the rows are rendering data cells, which are the actual components that are doing the data rendering.
So finish up with this component and you're going to see the result really shortly. I simply want to render the data here. Restarting our dev server, we can see a nice data table with four columns and three rows. So if you have tried building such data table, you would quickly come to the realization that it needs mostly client code on almost any level being for accessibility, for touching mouse or keyboard events, whatever.
So let's add some interactivity to our data table. We can add a click event to our data to have what to have selection like that. Oops, we get an error. This is a Next.js application and by default of the components are server ones. So to make this work so we can use the reactive state, we must add the use client directive over here and everything seems to be working. Clicking on the row covers it in blue. So we have some kind of reactivity. But if we explore our data table, we can see that there is a little bit of a markdown here in this cell.
4. Optimizing Customization and Syntax in Next.js
Building customization by incorporating a custom cell, resolving errors between client and server components, and optimizing syntax highlighting and markdown processing.
And how I proceed to build such customization is to providing a custom cell. So let's try to build this into our application. So I have prepared a markdown cell in advance. I will show it to you really shortly and I would pass it over here. Going to the table, we need to add it to the props interface and in the actual data row accepting it. We want to render it if provided instead of the default cell like that. Yeah.
So we have an error that we cannot pass a function to a client component. What this means is that our markdown cell is not a client component again. And our data row is a client component. And remember the two rules? Client components cannot render server components. So to make this work, we must add the use client directive over here. Again, the selection is working. But if we look closely, there is a code snippet inside the markdown.
And I'm kind of afraid of syntax highlighting, especially in markdowns, because they often come with some big, chunky library, which is responsible for the highlighting. So just to do a quick test, I would quickly build our application and I have prepared a Webpack plugin in advance to show us what would end up in our client bundle. So this would be the client bundle. Almost one third of it is related to syntax highlighting and markdown processing. How do we avoid that? How do we make this third party code stays on the server and never ends up on the client? Obviously, we cannot render a markdown on the server if our entire rendering tree is on the client. So what we would try to do is to achieve the composition pattern, but on a component level.
5. Enhancing Component Composition in Next.js
Achieving composition pattern at the component level by separating client and server parts, maintaining an agnostic component part, and optimizing rendering for client-specific functionalities.
So what we would try to do is to achieve the composition pattern, but on a component level. So if we try to separate the client parts of a component, in our case, that would be the data row, we should be left with something which I call an agnostic part, a part that could be rendered both as a client component or a server component. And we usually think of the composition pattern is that we must provide different components. But here we are going to split a single component into two parts. This would be our composition, a client part of the component and an agnostic part of the component. This is the visualization of what I will try to achieve through the code.
So we have the whole tree being agnostic and whenever we need a client code, we would simply wrap it around as a client shell. So how would that look like in our example with the data row? First, I want to delete this use client directive and then I would import a data row client component and I would try to wrap my agnostic rendering in this client part of my component, delete the use state and delete the event handling. Starting my dev server once again just to make sure everything works. Now I have to go and actually create our file that would be like that.
First, let's add the use client directive. I start from this template where instead of rendering conditional elements, I clone my agnostic part and simply add the client event handling and logic I want to achieve. In our case, that would be the selection. So with this in mind, let's see if this works. Yep, the click event is working. Now let's go over to our markdown cell and remove the use client directive to see if it still works. Yep, it still works. And if you do not trust me that this code would be left on the server, I would use a new utility, which is called server only. So I can import server only to make sure. And if you still don't believe me, we would do a quick build of our application. Giving it just a second.
6. Integrating Client-Server Performance Optimization
Achieving client-server performance optimization by keeping rendering tree agnostic, enabling separate client component extraction, and promoting interactivity within components.
Now the big chunky markdown and syntax highlight lighting library is left on the server, so it won't be shipped to our clients, which is the exact performance optimization that we're looking for. So getting back to our development server, what we basically achieved is by keeping the whole rendering tree agnostic, extracting the client parts, we can render server components deep down the tree. In our case, that will be the cell. And this allows us to keep the on click event on the row level. But again, the actual client logic, which adds the on click event, is in a separate component and does not interfere with the rendering tree at all. I want to zoom out a little bit to show you how different parts of your bigger component can interact with each other.
More specifically, the different client parts of your component can interact with each other. The example I have prepared is like let's do a single selection. So we would have a common client code, which keeps track to have only a single row selected. How I would approach this is I would add a client wrapper on my data table. So at the top of my component, again, I would wrap my default rendering with it like that. I will actually create the file. And starting with a loose client directive, this time I did not have to clone my agnostic part because I'm not adding anything. I'm just wrapping it in a context. So that would be state context. And I would keep the selection inside of it.
Wrapping my children inside of it, going now to the row client. Now, instead of keeping the state over here, I would be using the global state from my data table component like that. And I will be changing the selection from multiple to single. I would need an ID for this. And with this code, we should be able to have single selection. Yup, which is kept in our topmost component, that would be our data table. And the different client parts of our complex rendering tree would communicate with each other without interfering with the actual rendering tree. So the agnostic part, the important part, try to keep as much of your rendering tree agnostic. Try to extract client functionality in different files, which simply wrap your agnostic parts.
Comments