adobe/react-spectrum

`I18nProvider` is not marked as `"use client";`

Closed this issue ยท 5 comments

Provide a general summary of the issue here

When rendering the I18nProvider into a server side rendering framework like NextJS App Router (Tan-stack Start and others are likely also effected) the following error occures:

TypeError: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component

This is due to the component (and hooks) not being marked as a client component.

๐Ÿค” Expected Behavior?

The I18nProvider component should render as a client component. Allowing it to be initially rendered on the server and hydrated on the client and rendered as a wrapper within async Server Components.

๐Ÿ˜ฏ Current Behavior

React fails to render the component due to the Provider not being marked as a client component.

TypeError: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component

๐Ÿ’ Possible Solution

The quickest solution is to wrap the I18nProvider in a Higher Order Component (HOC) that contains a 'use-client' tag in the header

๐Ÿ”ฆ Context

I ran across this issue attempting to merge react-aria with next-intl to sync the accessible tags of the application with the language of the UI (English, Spanish, Arabic (RTL), and Chinese Simplifed for now ๐Ÿ˜„)

Going forward as React Server components become more popular and the industry starts sending valid, internationalized HTML over the wire rather than constructing it on the client dynamically I think that supporting imports in SSR components would be valuable for the react-aria library.

import { type AbstractIntlMessages, NextIntlClientProvider } from "next-intl";
import { I18nProvider as AriaI18nProvider } from "react-aria-components";

import { getLocale, getMessages } from "next-intl/server";

/**
 * I18nProvider is a wrapper component that provides the i18n context to the app.
 * It fetches the locale and messages from the server and provides them to the app.
 * It also provides the Aria i18n context to the app for accessible labels and ARIA attributes.
 */
export async function I18nProvider({ children }: { children: React.ReactNode; }) {
  const locale = await getLocale()
  const messages = await getMessages({ locale: locale })

  return (
    <AriaI18nProvider locale={locale}>
      <NextIntlClientProvider messages={messages} locale={locale}>
        {children}
      </NextIntlClientProvider>
    </AriaI18nProvider>
  );
}

๐Ÿ–ฅ๏ธ Steps to Reproduce

See the context above for a more in-depth example. However for a simple POC any create-next-app with the following file should do it:

import { I18nProvider } from "react-aria-components";

export async function Page() {
  return (
    <I18nProvider locale="en">
      <h1>Hello World</h1>
    </I18nProvider>
  )
}

Version

1.5.0

What browsers are you seeing the problem on?

Other

If other, please specify.

NodeJS

What operating system are you using?

MacOS / Alpine Linux

๐Ÿงข Your Company/Team

No response

๐Ÿ•ท Tracking Issue

No response

hmm if you import it through react-aria-components it should error already:

// Mark as a client only package. This will cause a build time error if you try
// to import it from a React Server Component in a framework like Next.js.
import 'client-only';

Maybe we need to add that to our other mono packages? Not sure why it wouldn't work anymore...

For prior context on why client-only instead of "use client": #5826

@devongovett Interesting, client-only is a optional dependency (https://www.npmjs.com/package/client-only) and not part of the React standard like "use client" is. Perhaps it needs to be added to RAC/RSP as a dependency?

On a side note: The standard way to handle the hydration/serialization issue alluded to above around the events is either to wrap them in a useEffect(() => {}, []) (as these only run on the client after first render and don't need to be serialized) or to render them conditionally in a if (typeof window !== 'undefined') { block.

Would there be interest in a contribution that updated the components to be RSC compatible? If so I'd love to work on a larger contribution to enable this. React-Aria is an incredible tool for building UI's for those with limited means and resources. Preventing SSR, while a cutting edge case now, can cause serious layout shift issues if the components are not rendered until after the page is loaded and the hydration step kicks in. Causing a terrible pitfall for users with motion or contrast accessibility concerns.

At a high level for this contribution: While the useEffect(() => {}) method is the most "correct" I'd argue in RAC/RSP's case its incorrect as it would only wire the events up after the page is first rendered and not during the initial render. if (typeof window !== 'undefined') { has the unpleasant side effect of potentially introducing hydration errors/warnings if the HTML changes. But for events which are mostly not immediately visible to the DOM until they are interacted with this is definitely a workable issue and would prevent the extra cycles/delay from useEffect(() => {}, [])s as well as be present and ready as soon as the DOM is interactive (post-hydration)

client-only and server-only are officially documented features: https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment

I don't think it's optional either?

On a side note: The standard way to handle the hydration/serialization issue alluded to above around the events is either to wrap them in a useEffect(() => {}, [])

This does not work with RSC because useEffect is not even exported in the react-server environment. It is only exported by React in client components.

Preventing SSR, while a cutting edge case now, can cause serious layout shift issues if the components are not rendered until after the page is loaded and the hydration step kicks in.

This is not the case. Client components are still SSRed to HTML, just like they were before RSCs. Server components only run on the server, whereas client components run on both the server and in the browser.

What import "client-only" means is that you must import from a file that already has "use client" or is already inside a client boundary. That's because events (e.g. onClick) are not serializable, so they cannot be sent from server components to client components over the network. Because most of our components require some kind of event handler, they must be rendered by a client component anyway. "use client" within our components would not work in this case, it needs to be in the file that renders our components.

Ah that explains the format of the error!

I misunderstood the details of the boundary. I understood client-only to tell nextJS to bypass the server rendering step entirely and only run the component render during hydration. Thank you so much for clarifying it! That would resolve my issue (importing it in a HOC to statisfy this contract)!!

My deepest apologies for the spammy ticket and may you have a great and relaxing holiday!