Preface#
As we know, in the React Server Component environment, the rendering runtime is always on the server, and in RCC, both environments may exist. To control that a library can only be used in one environment and throw an error in the other environment, we can use the client-only
or server-only
library.
Next.js is the first to support React Server Component and follows the server-module-conventions rfc, which will be discussed in the context of Next.js.
This rfc points out that in the package.json
file, the exports
field has added the react-server
export, and this field's export location will only be used by the reference chain in RSC.
For example, client-only/package.json
is defined as follows.
{
"exports": {
".": {
"react-server": "./error.js",
"default": "./index.js"
}
}
}
react-server
is written on the first line and is recognized first. So when client-only
is referenced in an RSC component, it actually references ./error.js
, which will cause an error.
Actual Use Cases#
In the new React architecture, the combination of RSC and RCC environments has become the norm. Therefore, for libraries, this feature is needed to be compatible with both environments.
Here we take next-intl as an example, which is an i18n library. The usage of this library is the same in both RSC and RCC. For example,
In RCC, we use useTranslations
like this.
'use client'
import { useTranslations } from "next-intl";
export default function Page() {
const t = useTranslations();
return // ReactNode
}
In RSC, we use useTranslations
like this.
import { useTranslations } from "next-intl";
export default async function Page({ params: { locale } }: PageParams) {
unstable_setRequestLocale(locale);
const t = useTranslations();
return // ReactNode
}
You may have noticed that the methods used in these two environments are the same, and the method exports are also the same. But you may have noticed that in RSC, hooks cannot be used, but there is no problem here.
This is related to the server-module-conventions mentioned earlier. The useTranslations
imported in RSC is not actually a hook, but just a regular function. However, to ensure the consistency of method calls, the name is also kept consistent.
Continuing to explore the package.json
of next-intl
, we find that it is defined as follows.
{
"exports": {
".": {
"types": "./dist/types/src/index.react-client.d.ts",
"react-server": "./dist/esm/index.react-server.js",
"default": "./dist/index.react-client.js"
}
}
}
And in RSC, it actually points to https://github.com/amannn/next-intl/blob/main/packages/next-intl/src/react-server/useTranslations.tsx. These are all implementations under RSC.
Use Cases in Business#
Here is an example of a recent scenario where we need to differentiate between ofetch instances in two scenarios. We know that the way to get cookies in Next.js RCC and RSC is different. In addition, the interfaces requested on pre-rendered pages are always sent out in RSC. We may need to attach some request information when the request is sent, such as authentication-related headers, user-agent, or forwarding the IP information of the actual requester, and so on.
In this case, we can write two different instances for the two environments.
First, create an internal package. For example, packages/fetch
.
Create package.json
.
{
"name": "@shiro/fetch",
"exports": {
".": {
"react-server": "./src/fetch.server.ts",
"default": "./src/fetch.client.ts"
}
},
"devDependencies": {}
}
Write fetch.server.ts
for RSC.
import 'server-only'
import { nanoid } from 'nanoid'
import { cookies, headers as nextHeaders } from 'next/headers'
export const $fetch = createFetch({
defaults: {
timeout: 8000,
onRequest(context) {
const cookie = cookies() // Use Next.js cookies() to get cookies
const token = cookie.get(TokenKey)?.value
const headers: any = context.options.headers ?? {}
if (token) {
headers['Authorization'] = `bearer ${token}`
}
context.options.params ??= {}
if (token) {
context.options.params.r = nanoid()
}
if (context.options.params.token || token) {
context.options.cache = 'no-store' // Do not tell Next.js to cache this request after authentication, as the data may leak
}
if (isDev) {
console.info(`[Request/Server]: ${context.request}`)
}
const { get } = nextHeaders() // Use the headers() method to get the original request headers
const ua = get('user-agent')
const ip =
get('x-real-ip') ||
get('x-forwarded-for') ||
get('remote-addr') ||
get('cf-connecting-ip')
if (ip) {
headers['x-real-ip'] = ip
headers['x-forwarded-for'] = ip
}
headers['User-Agent'] =
`${ua} NextJS/v${PKG.dependencies.next} ${PKG.name}/${PKG.version}`
context.options.headers = headers
},
onResponse(context) {
if (isDev) { // This is definitely ServerSide
console.info(
`[Response/Server]: ${context.request}`,
context.response.status,
)
}
},
},
})
Write fetch.client.ts
for RCC.
import 'client-only'
import Cookies from 'js-cookie'
function getToken(): string | null { // Use js-cookie to get cookies on the browser side
const token = Cookies.get(TokenKey)
return token || null
}
export const $fetch = createFetch({
defaults: {
timeout: 8000,
onRequest(context) {
const token = getToken()
const headers: any = context.options.headers ?? {}
if (token) {
headers['Authorization'] = `bearer ${token}`
}
headers['x-session-uuid'] =
globalThis?.sessionStorage?.getItem(uuidStorageKey) ?? uuid
context.options.params ??= {}
if (context.options.params.token) {
context.options.cache = 'no-store'
}
if (isDev && isServerSide) {
// eslint-disable-next-line no-console
console.info(`[Request]: ${context.request}`)
}
context.options.headers = headers
},
onResponse(context) {
if (isDev && isServerSide) { // It is still necessary to distinguish ServerSide here
console.info(`[Response]: ${context.request}`, context.response.status)
}
},
},
})
Now, both have the same methods and are exported. So we can use them in the business. To better support TypeScript, we need to modify "moduleResolution": "Bundler"
. Oh, don't forget to link this dependency in the package.json
of the project. For example, here, "@shiro/fetch": "link:./packages/fetch"
is added to the dependencies
in the project's package.json
.
Usage:
import { $fetch } from '@shiro/fetch' // The usage is the same in RCC and RSC
The above implementation has been implemented in Shiroi and is planned to be open-sourced to Shiro in the future.
I was recently laid off and I feel very depressed. Instead of reading textbooks and solving algorithms, I find it more meaningful to study and explore these things with a blank mind. 🥹😮💨, I can only say that it is cautious to choose a start-up company.
This article is synchronized and updated to xLog by Mix Space.
The original link is https://innei.in/posts/tech/exploring-environment-isolation-and-practice-of-react-server-component-and-react-client-component