`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:
react-spectrum/packages/react-aria-components/src/index.ts
Lines 13 to 15 in 50c7ada
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!