Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can not import and use Tab component within React Server Component on Next.js 13 #2021

Closed
judewang opened this issue Nov 17, 2022 · 3 comments
Assignees

Comments

@judewang
Copy link

What package within Headless UI are you using?

@headlessui/react

What version of that package are you using?

v1.7.4

What browser are you using?

Safari

Reproduction URL

https://github.com/judewang/headlessui-rsc

With commands to start dev server:

pnpm install
pnpm dev

Describe your issue

I've read #1981 and thought we could import components from headlessui and use it right in the server component, but it failed to compile.

I've tried some approaches to use Tab component as sample code within server component, like wrapping the exporting within a client component:

import { Tab } from "@headlessui/react"

export { Tab }

And it still failed.

Then I found 2 ways to make it work, one is wrapping all of the component that using Tab component as a big client component:

"use client"

import { Tab } from "@headlessui/react";

export const Content = () => {
	return (
		<Tab.Group>
			<Tab.List>
				<Tab>Tab 1</Tab>
				<Tab>Tab 2</Tab>
				<Tab>Tab 3</Tab>
			</Tab.List>
			<Tab.Panels>
				<Tab.Panel>
					<p>Tab 1 content</p>
				</Tab.Panel>
				<Tab.Panel>
					<p>Tab 2 content</p>
				</Tab.Panel>
				<Tab.Panel>
					<p>Tab 3 content</p>
				</Tab.Panel>
			</Tab.Panels>
		</Tab.Group>
	);
};

This approach can result to a big client component since it's hard to pass several server components through the client component's props, this makes all child components within this big client component become client components, too.

The other way is re-exporting Tab components individually and using it right in the Server Component:

// Tab.tsx

"use client"

import { Tab } from "@headlessui/react";

export const Group = Tab.Group;
export const List = Tab.List;
export const Panels = Tab.Panels;
export const Panel = Tab.Panel;
export { Tab };
// Any other components

import { Group, List, Panel, Panels, Tab } from "./Tab";

export const Content = () => {
	return (
		<div className="py-20">
			<Container as="section">
				<Group>
					<List>
						<Tab>Tab 1</Tab>
						<Tab>Tab 2</Tab>
						<Tab>Tab 3</Tab>
					</List>
					<Panels>
						<Panel>
							<p>Tab 1 content</p>
						</Panel>
						<Panel>
							<p>Tab 2 content</p>
						</Panel>
						<Panel>
							<p>Tab 3 content</p>
						</Panel>
					</Panels>
				</Group>
			</Container>
		</div>
	);
};

In my opinion, the second way may has better performance but the usage is different than described in documentation. Not sure is this need to be noticed in the usage section of the documentation or should the exporting structure need some revise?

@thecrypticace thecrypticace self-assigned this Nov 18, 2022
@thecrypticace
Copy link
Contributor

Most of our components rely on React Context and browser-specific APIs. React Server Components (RSCs) do not support context. This is because they're meant to run and communicate with server backends, perform sensitive tasks, fetching data asynchronously on the server, and eliminating unused client-side JS that only matters for an initial render, data fetching, or other backend-only tasks.

However, neither using Context nor any browser-specific APIs qualifies as this. No matter what you still need to ship Headless UI to the browser to be able to interact with the components. Now, we do support SSR in some limited capacity for some components but SSR itself is orthogonal to RSCs. When using them you get some SSR plus additional capabilities but you also have more restrictions on what you can and cannot do and use.

This is why, as of 1.7.4, @headlessui/react was marked as a client-side-only package. Because it's not intended to be used with server components. I would, as you mentioned above, only use these in components that have "use client"; which force RSCs to effectively ignore the restrictions and ship the JS to the browser. I believe SSR is still supported these cases as well (I'm not 100% sure but it wouldn't make sense to not support SSR with "use client"; since that is how React works by default today before RSCs were introduced).

Unfortunately, the only thing that using client-only does is give you a better build error when compiling. I do not believe there is a way for 3rd party packages to automatically be treated as client-only in the same manner that adding `"use client;" to your build does. Basically because it's a marker that a build tool looks for when compiling. Most of the time when including dependencies your build isn't necessarily traversing an entire AST (there may be minimal versions of this for ESM specifically for tree-shaking.. This is just a limitation of how RSCs work. I'm unsure if that will change in the future.

Bit of a lengthy explanation but I hope that explains how things work and why you can't just import them directly. I wish you could that would make things so much easier. 🤷

@judewang
Copy link
Author

judewang commented Nov 19, 2022

@thecrypticace Thank you for replying. When Vercel introduced the different between RSC and Client components, they said You can pass a Server Component as a child or prop of a Client Component. (but we cannot import a RSC directly within a Client component). In my case, most of the child components of Tab and Tab.Panel can be rendered as RSC rather than a SSR component which still need the hydration progress on the client. I would keep using Tab with my workaround described above to pre render the child components on the server.

CleanShot 2022-11-19 at 13 14 48

@suhaibmujahid
Copy link

In context of #2021 (comment):

Unfortunately, the only thing that using client-only does is give you a better build error when compiling. I do not believe there is a way for 3rd party packages to automatically be treated as client-only in the same manner that adding "use client;" to your build does. Basically because it's a marker that a build tool looks for when compiling. Most of the time when including dependencies your build isn't necessarily traversing an entire AST (there may be minimal versions of this for ESM specifically for tree-shaking.. This is just a limitation of how RSCs work. I'm unsure if that will change in the future.

Thank you for the detailed reply, @thecrypticace! It seems that things have been changed and libraries are expected to use "use client;". See https://nextjs.org/docs/getting-started/react-essentials#library-authors:

Library Authors

  • In a similar fashion, library authors creating packages to be consumed by other developers can use the "use client" directive to mark client entry points of their package. This allows users of the package to import package components directly into their Server Components without having to create a wrapping boundary.
  • You can optimize your package by using 'use client' deeper in the tree, allowing the imported modules to be part of the Server Component module graph.
  • It's worth noting some bundlers might strip out "use client" directives. You can find an example of how to configure esbuild to include the "use client" directive in the React Wrap Balancer and Vercel Analytics repositories.

Adding "use client;" in all react components should solve the problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants