Client and server interactions - vonschappler/Ultimate-React GitHub Wiki

"Breaking" the boundaries between Server and Client

Let's remember and think about how a traditional application is usually works.

In this case, we have to pieces on this puzzle, the back-end and the front-end, with a very clear boundary between them. Usually those are created using different technologies and not only that, they are mostly also hosted on different servers or platforms.

The communication between both is usually made using an API, created and hosted on the back-end server, from which the front-end is capable for fetching data in a json format.

As soon as this data is fetched, the back end "responsibility" ends, allowing the front-end to do its job. But this does not end here. The back-end is still needed when the front-end needs to run mutations on the data via post, put, delete and other requests.

Looking at the new modle, using React Server Components and Server Actions, things are different:

For this case, we have our server components and client components spread all over the component tree. It's safe to assume that the server components are part of our back-end, while client components part of our front-end, all mixed together, having no clear separation between back and front-end.

This creates a pattern called knitting, on which pieces of server and client code interweave allowing is to create full-stack application in a single code base, using a single react component tree.

This also remove the need of building an api to internediate the communication between back-end end front-end.

So, when it comes to fetching data, we already know that it's possbile to do it by simply fetching and rendering the data directly into de server component or pass this data from a server-component to a client-component as props.

As for mutations, we make use of Server Actions to perform actions on the server directly from client components. Using those mostly replace the put, post, delete and other methods we would be using to interact with the database in the traditional model.

Importing vs. Rendering

First, let's keep in mind that with this new RSC model, it's totally possible to go into deepber breaking the boundaries between server and client, considering situations in which we can render server components inside client components, as long we can pass the server component as a prop to the client component.

What happens in the background for allowing us this kind of operation is the fact that when the parent client component is importing it's children server component which was already rendered - as discussed before - remember that for this case scenario, the whole component tree renders server components first, leaving the "blank holes" for client components. So when the hole needs to be filled, the children server component was already rendered.

But for this to possible, the top parent of both components in this case need to import both components, creating what we call dependecy tree, which is a bit different from the component tree, because it shows the direct dependency link between the modules required for each component to be rendered in the component tree.

NOTE:

It's exactly in this dependency tree where we define the boundaries between server and client.

It's important to remember that:

  • Client components can IMPORT only other client components
  • Client components can RENDER both client and server components, if those are passed as props.
  • Server components can IMPORT and RENDER both client and server components
  • A server component can be both server and client component: a component is nothing more than a blueprint which can be used and reused though our code thought different intances, and like so, if we import a server component inside a client component it will automatically be converted into a client component

Working with client components in server components

So far in our application we have been using client components inside server components. Though a few additional notes should be taken:

  • Client componentns are the components responsible for the interactiveness of the application, hance server components do not carry any js code with them during rendering (as stated before)
  • Client components (as also stated before) need the declarative "use client"; at the begining of their files
  • Client components should be as low as possible into the component tree, because we know that all child of client components will be converted into client components, that would cause some impact into the render process of some server components which could be rendered directly on server

Keep in mind that all for all this discussion we are using the word COMPONENT, just as a simplification of COPONENT INSTANCE.

Using Next.js custom hooks in client components

Even if we are using Next.js hooks, compnents will only work correctly IF they are client components, because a Next.js hook, is a hook, and as already discussed, server components can't run hooks.

Sharing state between client and server

We already know that we can pass data from the server to the client as props. But how would be possible for us to share data from the client to be server?

Sharing some state that we create on a client component to a server components is not that easy to be implemented. On of the easiest ways of doing so, can be achieved by storiong the state in the URL.

This can be done easly using a page.js file because this has access to both params and searchParams objects, making it easy for us, developers to manipulate the URL as we please, and in this case, as a state manager.

The logic on how to implement this can be achieve using Next.js custom hooks and URLSearchParams browswer API:

'use client';

// other imports

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

export default function ChangeURLState() {
  const query = useSearchParams();
  const router = useRouter();
  const pathname = usePathname();
  // optionally it's possible to add a fall back here
  const active = query.get('param') ?? default_param_value;

  function handleURLState(value) {
    const params = new URLSearchParams(searchParams);
    params.set('param', value);
    // adding {scroll: false} here prevents page default scrolling to the top from happening
    router.replace(`${pathname}?${params.toString()}`, { scroll: false });
  }

  return <button handleClick={() => handleParam('value')}>Set State</button>;
}

or a simpler approach (which can be used in some specific scenarions) like so:

'use client';

// other imports

import { usePathname, useRouter } from 'next/navigation';

export default function ChangeURLState() {
  const path = usePathname();
  const router = useRouter();
  function handleURLState(value) {
    const url = path.concat(`?param=${value}`);
    router.replace(url);
  }

  return <button handleClick={() => handleParam('value')}>Set State</button>;
}

Working with server components inside client components

As discussed before, we are aware that client components cannot import server components. But there may be situations in which we require a server component inside the client component.

Previously, we had a whole overview to understand the differences between importing and rendering, in which we got to the conlusion that client components can RENDER but not IMPORT server components as long the server component is passed to it as a prop.

With this in mind, all that we need to consider is to really make our client component accept the server component as a prop (usually the children prop, but it can be something else that you come up with) and use both as imports on the closest parent to both components.

This works perferctly for those cases because:

  • The closest parent will be also a server component
  • The rendered server component inside the client component will already be rendered by the server, and returned to is as React Component
  • We'll not be crossing the server-client boundary defined when we declare a component as client (using the use client directive)
// servercomponent.js

export default async function ServerComponent() {
  // content of the component which fetches some data with its return function
}
// clientcomponent.js
export default function ClientComponent({ children }) {
  return <>{children}</>;
}
// closestparent.js (usually a page.js file)
// The closest parant can be  any other synchronous or asynchronous server component

import ClientComponent from 'path/to/ClientComponent';
import ServerComponent from 'path/to/ServerComponent';

// other imports

export default function ClosestParent() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  );
}

It's important to note that when ti comes to fetching data strategy, this can be done in multiple ways, like for example making multiple requests on the components that need the same data, making use of Promise.all(), or fetching the data once in the closest parent to a all the components which need the same data and just pass it as a prop - it the developers decision in which strategy to use based on the project we are working.


Using Context API for State Managment

There are many possibilities on how manage state on a Next.js application. We already discussed one of them, which was sharing the state on the URL, which is used when we need to share state between server components and client components.

But when we need to share state between client components, two different options would fit better, because remember that sharing state in the URL would cause new re-rendes causing possibly some unwanted data re-fetching:

  1. Creating a parent component to wrap the client components which need the state shared between them and passing that as a prop
  2. Using React context API, as a client component, which will take advantages of all rendering features already discussed if placed inside the root layout.js file as a global context, enabling the use of the custom hook defined in the context to all other client components in the application

Creating API's in Nexj.js with Route Handlers

Before we begin, we need to keep in mind that when using the app router, defining api routes/endpoints is no longer necessary, because with the app router we have access to server actions if we wish to mutate data.

But to work with pages router, this is still necessary and so let's understand what is a route handlers and see how it's possible to create api endpoints with Next.js.

Following yet another Next.js convention, a route handlers is the file names routes.js, which can be placed inside ANY FOLDER, in which we don't have a page.js file. This is important, because no HTML should be rendered when a request hits the specified endpoints defined in the routes.js file, because those enpoints will already send some json data to client (and sending both a json data and a HTML file will cause unwanted conflicts on the application).

So, one of the best suggestions to work with the route handler is to create a new folder, as a route folder, with the route.js file inside it.

Inside this file we can define and export one or more functions for each one of the http requests types. The functions can use the web apis (check MDN docs) Response and Request, or the enchanced version of them, provided by Next.js.

Below we have an example of a simple route.js file, from which we can make get requests:

// the http get request have access to request, the params object, which can be passed as arguments to the function
export async function GET(req, { params }) {
  return Response.json({ message: 'Hello from /api' });
}
⚠️ **GitHub.com Fallback** ⚠️