19-Dec

React, Development and JavaScript

Slik lager du et bibliotek som støtter React Server Components

React server components vinner popularitet og det er avgjørende for komponentbiblioteker å støtte det for å holde tritt. Hvordan støtter du det?

5 min read

·

By Marcus Haaland

·

December 19, 2023

React server components vinner popularitet for deres ytelsesforbedringer, og det er avgjørende for komponentbiblioteker å holde tritt for å forbli aktuelle.

Men her står vi overfor et paradoks: til tross for viktigheten, er det lite veiledning å finne. Selv i Reacts egen dokumentasjon, er omtalen av hvordan biblioteker kan støtte serverkomponenter diskret plassert som et listet punkt under en vilkårlig seksjon. I denne guiden vil jeg lose deg gjennom prosessen i å transformere et eksisterende komponentbibliotek til å også støtte klient- og serverkomponenter.

KRÆSJ

Skjermdump av feilmelding
Denne feilmeldingen sier at vi bruker klientkode i en komponent som ikke er markert som klientkomponent.

Jeg ble møtt av denne feilmeldingen da jeg skulle importere en Heading fra komponentbiblioteket mitt inn på hjemmesiden min.

// Home.tsx
import { Heading } from "design-react";

export function Page() {
 <Heading>Home</Heading>
};

Dette syns jeg var overraskende. Heading-komponenten var jo en enkel komponent uten noe klientkode, så det burde slettes ikke være noe klaging på klientkode som kjører på serveren.

Hva har gått galt?

Hva er egentlig problemet?

Resten av komponentbiblioteket ser slik ut: en enkel Heading-komponent, som brukt over. Og en Button som bruker useState, altså en klientkomponent. Derfor har jeg markert Button-filen med “use client”-direktivet, for å sikre at komponenten behandles som en klientkomponent.

// design-react

// Heading.tsx
export function Heading() {}

// Button.tsx
"use client";
export function Button() {}

// index.ts
export * from Heading;
export * from Button;

Det ser da bra ut, det?

Heading-komponenten trenger ikke “use client”-direktivet, siden det er en serverkomponent. Og om jeg bruker Button, bør det behandles som en klientkomponent, og ikke forårsake noen ubehagelig feilmelding.

Selv med disse tiltakene, er det to årsaker til at feilen fortsatt oppstår:

Den første årsaken er at klientkode kjører på serveren. Det er kanskje overraskende, ettersom på hjemmesiden importerer vi kun serverkomponenten Heading. Den overraskende realiteten er at Heading er i samme chunk som Button, og hele chunken vil prøve å bli kjørt.

Normalt sett hadde ikke det vært noe problem at også Button kjører, siden Button er markert å kjøre på klienten.

Men så kommer vi til andre årsak til feilen, som er hvordan bundlere håndterer koden. De flytter ofte import-setninger til toppen, som kan skyve “use client”-direktivet nedover i filen. Dette direktivet må være øverst for at komponenten skal anerkjennes som klient-spesifikk. Feilplassering fører til at både klient- og serverkomponenter kjøres på serveren, noe som fører til kræsj.

Løsningen

Vi har dermed to årsaker til feilen:

  1. én chunk fører til at klient- og serverkomponenter kjøres på samme sted, og
  2. bundleren flytter på “use client”-direktivet, som fører til feil kategorisering av type komponent.

Løsningen til dette er å splitte opp klient- og serverkomponenter i hver sin chunk. Og å flytte “use client”-direktivet til toppen av filen.

Splitte opp chunken

Ved å splitte opp bundelen er vi sikre på at riktig chunk kjører i riktig miljø. Så vi går fra å ha én index.ts hvor vi importerer alle filer, til å splitte opp importeringen i clientComponents.ts og serverComponents.ts.

// clientComponents.ts
export * from Button;

// serverComponents.ts
export * from Heading;

Vi må også si ifra til bundleren at vi nå går fra å ha én entry til å ha to. Jeg brukte Vite/Rollup som bundler, men tilsvarende config finnes også i andre bundlere:

// vite.config.ts

export default defineConfig({
  build: {
    lib: {
      entry: [
        path.resolve(__dirname, "clientComponents.tsx"),
        path.resolve(__dirname, "serverComponents.tsx")
      ],
      formats: ["es"],
      fileName: (format, name) => {
        return `${name}.js`;
      }
    },
  },
});

Vi må også oppdatere exports i package.json, så konsumere av pakken kan importere komponenter fra riktig entry:

// package.json

"name": "design-react",
"exports": {
    "./clientComponents": {
      "import": "./dist/clientComponents.js"
    },
    "./serverComponents": {
      "import": "./dist/serverComponents.js"
    }
  },
  "files": [
    "dist"
  ]

Vi har nå splittet opp kompontenene i ulike chunks, men dette krever en justering for å støtte riktig eksportering av typer.

Sidespor om TypeScript

TypeScript genererer en egen typefil for hver chunk: clientComponents.d.ts og serverComponents.d.ts. Ettersom "types" i package.json kun støtter å referere til én typefil, må vi løse typer for chunkene på en annen måte.

Løsningen er å benytte typesVersions. Feltets intensjon er å sette ulike type-definisjoner avhengig av TypeScript-versjon, men vi kan også bruke det til å peke på hvor man finner type-filen avhengig av modul:

// package.json
"typesVersions": {
  "*": {
    "clientComponents": [
      "./dist/types/clientComponents.d.ts"
    ],
    "serverComponents": [
      "./dist/types/serverComponents.d.ts"
    ]
  }
},

Vi har splittet opp chunkene og justert typene. Som konsumer av pakken, kan jeg nå importere fra rett filsti for å unngå å bli overfalt av en feilmelding:

// Home.tsx
import { Heading } from "design-react/serverComponents";

MEN vi har bare løst halvparten av problemet. Bruker du en knapp fra clientComponents vil siden kræsje. Vi trenger fortsatt å fikse “use client”-direktivet.

Legg direktivet i toppen av filen

For å legge til “use client”-direktivet i toppen av en fil kan vi bruke et verktøy som er tilgjengelig i mange bundlere: banner.

Banner er ofte brukt til å skrive info om lisens for koden, men vil i vårt tilfelle brukes til å legge “use client”-direktivet på toppen av chunken. Faktisk er det dette komponentbiblioteket Chakra UI bruker for å markere sine komponenter som klientkomponenter.

Vi trenger da å oppdatere bundler-configen. Vi legger til “use client”-direktivet kun for clientComponents-chunken:

// vite.config.ts

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        banner: (chunkInfo) => {
          if (chunkInfo.name === "clientComponents") {
            return "'use client';";
          }
          return "";
        }
      }
    }
  },
});

Nå kan vi også bruke Button-komponenten uten verken et ekstra “use client”-direktiv eller at siden kræsjer.

// Home.tsx
import { Heading } from "design-react/serverComponents";
import { Button } from "design-react/clientComponents";

export function Page() {
  <>
    <Heading>Home</Heading>
    <Button>Boop</Button>
  </>
};

Konklusjon

Vi har nå løst to hovedutfordringer for å støtte React Server Components i et bibliotek. Først oppdaget vi hvordan vi kan dele opp komponenter i separate chunks for klient og server, så vi kjører riktig kode i riktig miljø. For det andre, lærte vi hvordan bruken av “banner”-verktøyet i bundlere kan sikre at “use client”-direktivet forblir øverst i filen, som er kritisk for at det oppfører seg som forventet. Ved å følge disse trinnene, kan også du ta i bruk React Server Components for bedre ytelse.