Using client components
In this tutorial, you will learn how to use client components to create interactive elements in your application. Enhance your user experience with client-side rendering and dynamic content updates.
We will build a simple photo gallery application that will use client components for navigation and image display. You will learn how to create client components and handle user interactions.
We will also use the file-system based routing solution provided by the framework to create routes for our application. This will allow us to navigate between different pages and display the appropriate content.
To get started, create a new React application using the following command:
mkdir photos
cd photos
pnpm init
pnpm add @lazarv/react-server react-click-away-listener @faker-js/faker
pnpm add -D @types/react @types/react-dom autoprefixer postcss tailwindcss typescript typescript-plugin-css-modules
pnpx tailwindcss init -p
mkdir src
mkdir src/app
mkdir src/components
We need to modify tailwind.config.js
to include the following configuration:
tailwind.config.js/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/components/**/*.{js,ts,jsx,tsx}",
"./src/app/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};
With these changes we enabled Tailwind CSS to scan our components and app directories for styles to include in the final build and added support for radial and conic gradients.
Create the global.css
file needed for importing Tailwind CSS styles in the src/app
directory:
src/app/global.css@tailwind base;
@tailwind components;
@tailwind utilities;
Our tsconfig.json
will look like this:
tsconfig.json{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"jsx": "preserve",
"strict": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["react/experimental", "react-dom/experimental"],
"module": "ESNext",
"moduleResolution": "Bundler",
"plugins": [{ "name": "typescript-plugin-css-modules" }]
},
"include": ["**/*.ts", "**/*.tsx", ".react-server/**/*.ts"],
"exclude": ["**/*.js", "**/*.mjs"]
}
We enabled the typescript-plugin-css-modules
plugin to support CSS modules in our project and we also include .react-server/**/*.ts
files in our project to enable typed routing when using the file-system based router with the built-in Link
component.
When using the file-system based router we can specify the root directory for our routes in the react-server.config.json
file so that the router knows where to look for the routes without crawling the entire project directory tree. This is especially useful for larger projects with many directories and files. Create the react-server.config.json
file in the root of your project with the following content:
react-server.config.json{
"root": "src/app"
}
Let's start by creating a new file called src/photos.ts
with the following content:
src/photos.tsimport { faker } from "@faker-js/faker";
export type Photo = {
id: string;
username: string;
imageSrc: string;
};
const photos: Photo[] = new Array(9).fill(null).map((_, index) => ({
id: `${index}`,
username: faker.internet.userName(),
imageSrc: faker.image.urlPicsumPhotos(),
}));
export default photos;
This file will generate an array of 9 random photos using the @faker-js/faker
package. Each photo will have an id
, username
, and imageSrc
property. This will be our random photo set for the gallery, our data source for the application.
In this simple photos app we will have only one main route that will display the gallery and a router outlet for rendering a modal view of a single photo.
We want to wrap all of our pages into an HTML document layout, so we need to create the src/app/(root).layout.tsx
file with the following content:
src/app/(root).layout.tsximport "./global.css";
export default function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>Photos</title>
<meta
name="description"
content="A sample app showing dynamic routing with modals as a route."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
{children}
</body>
</html>
);
}
This layout will be used for all routes in our application. It includes the global CSS styles and sets the title and description of the page. This is a React Server Component that will be rendered only on the server-side.
We named the layout file (root).layout.tsx
because it will be used as the root layout for all routes in our application. (root)
is only a name for us to identify the file better as the router will omit all parts of the file name which are in parentheses. The instrument the router to use our file as a layout by using the .layout.tsx
suffix. The router will apply this layout to all subsequent routes.
To create our main gallery view and our index page, we need to create the src/app/page.tsx
file with the following content:
src/app/page.tsximport { Link } from "@lazarv/react-server/navigation";
import swagPhotos from "../photos";
export const ttl = 30000;
export default function Home() {
const photos = swagPhotos;
return (
<main className="container mx-auto">
<h1 className="text-center text-4xl font-bold m-10">Photos</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 auto-rows-max gap-6 m-10">
{photos.map(({ id, imageSrc }) => (
<Link
key={id}
to={`/photos/${id}`}
prefetch
ttl={30000}
rollback={30000}
>
<img
alt=""
src={imageSrc}
height={500}
width={500}
className="w-full object-cover aspect-square"
/>
</Link>
))}
</div>
</main>
);
}
This page will display the gallery of photos. We use the @lazarv/react-server/navigation
module to create a link to each photo in the gallery. The prefetch
, ttl
, and rollback
props are used to optimize the navigation experience by preloading the photo data and caching it for a certain amount of time.
You can notice, that the to
prop of the Link
component is set to /photos/${id}
. This is the path that the router will use to match the route and render the appropriate component for dispaying the photo. This to
prop is also type-safe and will be checked against the routes defined in the src/app
directory.
The default exported function Home
is a React Server Component that will be rendered on the server-side. But the Link
component is a client component that will be rendered on the server-side and then it will get hydrated on the client-side. This is how we can mix server and client components in our application with ease. The image element used as the child of the Link
component is rendered only on the server-side. Only components annotated with the "use client";
directive will be loaded on the client-side to be able to use any client-side features or state.
Using the Link
component our new page will be loaded using client-side navigation and the browser will not reload the page when navigating between the photos. The payload of the page where the user will navigate will be not an entire HTML document, but only the React Server Component payload needed to render the page. This will make the navigation faster and more responsive.
Let's create our first own client component that will display a modal view of a single photo. Create the src/components/modal/Modal.tsx
file with the following content:
src/components/modal/Modal.tsx"use client";
import { useEffect } from "react";
import ClickAwayListener from "react-click-away-listener";
export default function Modal({ children }: { children: React.ReactNode }) {
useEffect(() => {
document.body.classList.add("overflow-hidden");
return () => document.body.classList.remove("overflow-hidden");
}, []);
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
history.back();
}
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
return (
<div className="fixed z-10 left-0 right-0 top-0 bottom-0 mx-auto bg-black/60">
<ClickAwayListener onClickAway={() => history.back()}>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-3xl sm:w-10/12 md:w-8/12 lg:w-1/2 p-6">
{children}
</div>
</ClickAwayListener>
</div>
);
}
The most important part is the first line of the file: "use client";
. This directive tells the framework that this component should be loaded on the client-side. This is how we can create client components. The rest of the file is a simple modal component that will display the photo in a modal view. We use the useEffect
hook to add and remove the overflow-hidden
class from the body
element to prevent scrolling when the modal is open and another useEffect
hook to handle keyboard navigation. We also use the ClickAwayListener
component from the react-click-away-listener
package to close the modal when the user clicks outside of it. You don't need to specify "use client";
directive for the ClickAwayListener
component, because it is imported by our Modal
component which is already a client component.
In client components, you can use all of the React hooks only working on the client-side, like the useEffect
hooks to handle browser events and you can also use React components which needs to run on the client-side, in the browser, like the ClickAwayListener
component from the react-click-away-listener
package.
The children
prop could be any type of React component. It can be a React Server Component or another client component. In this case, we will use it to display the photo in the modal view.
All client components are rendered on the server-side and then they are hydrated on the client-side. This means that the server will render the component and send it to the client, where it will be re-rendered and the client-side logic will be applied.
Now we need to create a route for displaying a single photo in a modal view. Create the src/app/@modal/photos/[id].page.tsx
file with the following content:
src/app/@modal/photos/[id].page.tsximport Frame from "../../../components/frame/Frame";
import Modal from "../../../components/modal/Modal";
import photos from "../../../photos";
export default function PhotoModal({ id: photoId }: { id: string }) {
const photo = photos.find((p) => p.id === photoId);
return (
<Modal>{!photo ? <p>Photo not found!</p> : <Frame photo={photo} />}</Modal>
);
}
This page will display a single photo in a modal view. We use the @modal
pattern in the directory name to tell the router that this route should be rendered as an outlet named modal
, so it can be used over the main gallery page. The photos
array is imported from the src/photos.ts
file and we use the photoId
parameter to find the photo with the matching id
.
The Frame
component is a simple component that will display the photo in the modal view. Create the src/components/frame/Frame.tsx
file with the following content:
src/components/frame/Frame.tsximport { Photo } from "../../photos";
export default function Frame({ photo }: { photo: Photo }) {
return (
<>
<img
alt=""
src={photo.imageSrc}
height={600}
width={600}
className="w-full object-cover aspect-square col-span-2"
/>
<div className="bg-white p-4 px-6">
<p>Taken by {photo.username}</p>
</div>
</>
);
}
This component is a React Server Component. It will display the photo and the username of the user who took the photo.
Now if we go back to our outlet route, we can see that we are using the Frame
component to display the photo in the modal view using our Modal
client component. We can mix React Server Components and client components in our application to create dynamic and interactive user interfaces and optimize the rendering process. We only need to use client components for interactive or dynamic parts of our application, like modals, popups, JavaScript-driven animations, or other client-side features.
Now we can return to our layout component and include our new outlet to render the modal when the route matches the /photos/[id]
pattern. Update the src/app/(root).layout.tsx
file with the following content:
src/app/(root).layout.tsximport "./global.css";
export default function Layout({
modal,
children,
}: React.PropsWithChildren<{
modal: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>Photos</title>
<meta
name="description"
content="A sample app showing dynamic routing with modals as a route."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
{children}
{modal}
</body>
</html>
);
}
Layouts using the file-system based router will receive all subsequent outlets as props. The modal
prop will contain the component rendered by the modal outlet when the route matches. We can use this prop to render the modal view in our layout component over the gallery view.
We can also optimize the navigation by using the ReactServerComponent
component to enable client-side navigation for the modal view. We can use the outlet prop for initial content on a page refresh. Using ReactServerComponent
together with the Link
component will result in using a smaller network payload as the link will update the modal outlet just by rendering the outlet only on the server-side as RSC payload. The payload will be 0.1x only of the size of re-rendering the whole page as RSC, getting from ~15k to only ~1.5k in size. Imagine how much impact this can have on a much more complex application. Update the src/app/(root).layout.tsx
file with the following content:
src/app/(root).layout.tsximport { ReactServerComponent } from "@lazarv/react-server/navigation";
import "./global.css";
export default function Layout({
modal,
children,
}: React.PropsWithChildren<{
modal: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>Photos</title>
<meta
name="description"
content="A sample app showing dynamic routing with modals as a route."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
{children}
<ReactServerComponent outlet="modal">
{modal}
</ReactServerComponent>
</body>
</html>
);
}
To run the application, add the following scripts to your package.json
file:
package.json{
"scripts": {
"dev": "react-server",
"build": "react-server build",
"start": "react-server start"
}
}
Using the file-system based router, we no longer need to specify the entrypoint of our application. The router will automatically find the routes in the src/app
directory and render the appropriate component for each route.
Now you can run the application using the following command:
pnpm dev
Open your browser and navigate to http://localhost:3000
to see the photo gallery application. Click on any photo to open the modal view and see the photo in a larger size.
To build the application for production, run the following command:
pnpm build
You can start the production server using the following command:
pnpm start
In this tutorial, you learned how to use client components to create interactive elements in your application. You built a simple photo gallery application that uses client components for navigation and image display. You also learned how to create routes for your application using the file-system based routing solution provided by the framework. You can also find this example application in the GitHub repository and after cloning the repo and installing the dependencies by executing pnpm install
you can run the application using the pnpm --filter ./examples/photos dev
command.