next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Next.js 13 - appDir - Error: Unsupported Server Component type: undefined

Open DuCanhGH opened this issue 3 years ago • 7 comments

Verify canary release

  • [X] I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
  Platform: win32
  Arch: x64
  Version: Windows 10 Pro for Workstations
Binaries:
  Node: 18.12.0
  npm: N/A
  Yarn: N/A
  pnpm: N/A
Relevant packages:
  next: 13.0.0
  eslint-config-next: N/A
  react: 18.2.0
  react-dom: 18.2.0

What browser are you using? (if relevant)

Chrome 106.0.5249.119 / Firefox Developer Edition 107.0b5

How are you deploying your application? (if relevant)

next start, Vercel (https://test-next-13-appdir.vercel.app/)

Describe the Bug

This issue only appears in dev mode

When I import a client component (the file has "use client" directive) from its exact location (let's say ./src/components/Foo/Bar.tsx) and then use it in appDir, everything works fine, the component loads and there's no issue.

But when I reexport it in ./src/components/Foo/index.ts with export * from "./Bar" and then import it in appDir like this: import { Bar } from "@/components", suddenly appDir stops working and then Next shows an issue like this: Error: Unsupported Server Component type: undefined (https://ibb.co/y01RPBv). I tried console.logging the component and what was logged is just undefined. The production build doesn't have anything of the sort though, everything works just fine.

Thank you :)

Expected Behavior

Everything should work just fine, the component should display as normal.

Link to reproduction

https://github.com/DuCanhGH/next-13-server-component-undefined

To Reproduce

  • Run pnpm i
  • Run pnpm dev
  • Go to http://localhost:3000 and it should fail to load the page. I find it to be reproducible on both Chrome and Firefox

DuCanhGH avatar Oct 27 '22 07:10 DuCanhGH

This is "solved" if you add the "use client" directive to the top export (src/components/index.ts in this case).

awareness481 avatar Oct 27 '22 08:10 awareness481

@awareness481 it really did, doesn't seem to be neat, but it works for now :D

DuCanhGH avatar Oct 27 '22 08:10 DuCanhGH

Any updates on this? I have a monorepo structure. When using barrel index.ts files in a library/package to re-export components, I get the same error.

Having to set "use client" on every index.ts file is not ideal.

HARAJLI98 avatar Oct 31 '22 21:10 HARAJLI98

@HARAJLI98 I haven't found a better solution as well, so for now I'm currently having a ./client.ts for components that use "use client", and I plan to merge that file into ./index.ts when this gets resolved.

DuCanhGH avatar Nov 01 '22 01:11 DuCanhGH

Adding "use client" to each index.ts is not the ideal solution, because there may be components that are not client components, which can cause new problems when I use non-client components, such as when I use a server component and pass a function into the component

coderlfm avatar Nov 15 '22 03:11 coderlfm

@coderlfm you can add a client.ts or a client folder with a index.ts and put all the files that are Client Components in it just like how I currently do it. I don't think this issue will be fixed anyway.

DuCanhGH avatar Nov 15 '22 04:11 DuCanhGH

@awareness481 it really did, doesn't seem to be neat, but it works for now :D

Thanks for the advice. I'm doing the same thing now

coderlfm avatar Nov 15 '22 05:11 coderlfm

In my case this really won't be ideal because I am currently working with firebase/firestore which is providing values authentication, which I am using in the main layout.tsx, so adding use client in there, the way I see it will cause all the components I will import to be client components which isn't ideal at all. Can anyone recommend a solution besides the one I am currently using?

richardHaggioGwati avatar Nov 21 '22 14:11 richardHaggioGwati

@richardHaggioGwati I don't really understand why you have to put a use client in the main layout.tsx...

DuCanhGH avatar Nov 22 '22 02:11 DuCanhGH

I'm using React context api, and I have placed the provider inside of the main layout.

After releasing the problem, I have started working on a different implementation

richardHaggioGwati avatar Nov 22 '22 05:11 richardHaggioGwati

@richardHaggioGwati I'm having no problem using React's Context API in appDir without adding a use client directive to the main layout.tsx. You just have to isolate the provider into a .tsx / .jsx file, add a use client to the top of that file, import the provider into the main layout.tsx and wrap your app with it. You can only use useContext within client components, however.

DuCanhGH avatar Nov 22 '22 08:11 DuCanhGH

Thanks I will keep that in mind and proceed to do so

richardHaggioGwati avatar Nov 22 '22 09:11 richardHaggioGwati

I'm seeing the same error when trying to import a client compound component in a server component, eg:

import List from './List';

export default function Home() {
  return (
    <List>
      <List.Item>Hello</List.Item>
    </List>
  );
}

Where List is a client component that exposes another component on the List.Item property:

'use client';
import React from 'react';

function List({ children }: { children: React.ReactNode }) {
  return <ul>{children}</ul>;
}

function ListItem({ children }: { children: React.ReactNode }) {
  return <li>{children}</li>;
}

List.Item = ListItem;
export default List;

Here's a link to a reproduction: https://stackblitz.com/edit/vercel-next-js-jbt5nq?file=app/page.tsx

margalit avatar Nov 23 '22 09:11 margalit

@margalit I don't think your issue is related to mine, though yours probably happens because List doesn't contain Item on the server (when you console.log(<List />) you will see $$typeof, filepath, name, async if it is a client component, yet it will have $$typeof, render, Item if it is a server component). I don't really think that component needs to be a client component though, but maybe your actual component is more complex than this. If that's true then you can do it like this:

  • Have a List/index.ts like this:
import { _List, ListItem } from "./client";

export const List = Object.assign(_List, {
    Item: ListItem
});

and a List/client.tsx like this:

"use client";
export function _List({ children }: { children: React.ReactNode }) {
    return <ul>{children}</ul>;
}

export function ListItem({ children }: { children: React.ReactNode }) {
    return <li>{children}</li>;
}
  • Then in your page, do this:
import { List } from "./List";

export default function Home() {
  return (
    <List>
      <List.Item>Hello</List.Item>
    </List>
  );
}

It should work just fine. Hope this helps :)

Edit: it seems that this trick no longer works with Next's recent builds. Maybe you have to do one of these instead:

  • Just export ListItem.
  • Import the namespace component into a client component, then use that component in a RSC:
"use client";
import { List } from "./List";

export function ClientComponent() {
  return (
    <List.Item>Hello</List.Item>
  )
}

then in your page:

import { ClientComponent } from "./ClientComponent";
import { List } from "./List";

export default function Home() {
  return (
    <List>
      <ClientComponent />
    </List>
  );
}

Both of these doesn't feel as nice to use as before to be honest, but it seems that there just is no way to do it that way anymore.

DuCanhGH avatar Nov 23 '22 10:11 DuCanhGH

I just started a new nextjs v13.0.5 app with no typescript and got the same error as above but for a very different reason. I imported a component with {} in the import definition (import {TestComp} from '@comps/TestComp';) but in my component file the export was "default". I simply removed the {} and the error was removed. Interesting, because react gives a better error message that directly specifies that this indeed is the casue of the error.

cwikio avatar Nov 24 '22 18:11 cwikio

@cwikio that's why you need Typescript 💀

DuCanhGH avatar Nov 24 '22 18:11 DuCanhGH

Solving this with that index.ts hack is a neat workaround but I would really like to get to a better solution, since this would mean you can't really setup something like a design-system package and do import { Button } from '@ui'; without having every component setup as a client component if just one would happen to be one or having to use more explicit imports.

I've ended up splitting my UI package internally into an export for client and one for server components with the help of a modified ctix version to still generate those index files automatically. The imports are now from @ui/clientComponent or @ui/component, while importing from @ui is no longer possible, although both are still in the same package.

jookshub avatar Dec 07 '22 04:12 jookshub

@jookshub I also do '@/components' and '@/components/client' and it's kinda fine tbh. It's an issue with the development build only (at least it was last time I checked), so it probably isn't something intended to happen, maybe I can go and investigate it so that we don't have to do this anymore...

DuCanhGH avatar Dec 07 '22 04:12 DuCanhGH

Same issue here with Next 13.1 and Typescript. I've found three workarounds for this:

  1. use default export MyComponent
  2. don't reexport the component through an index: export * from './MyComponent'
  3. if you need the index add 'use client' in that index file:
'use client'

export * from './MyComponent'

diegosanz avatar Jan 27 '23 09:01 diegosanz

a workaround for me:

from export * from './my-component'

to export { MyComponent } from './my-component'

irekrog avatar Feb 07 '23 09:02 irekrog

@irekrog sounds great! I wonder how it just fixes the issue though.

DuCanhGH avatar Feb 07 '23 17:02 DuCanhGH

@diegosanz that's nice, I don't recommend exporting Shared/Server Components through files that have "use client" at the top of it though. I personally prefer a client.ts for Client Components and a index.ts for Shared/Server Components tbh.

DuCanhGH avatar Feb 07 '23 17:02 DuCanhGH

Also having this problem with compound components I prefer the method of using Object.assign and that doesn't work. Just wanted to weight in that this is still a problem and causes codebases to get clustered with index files as a workaround or discourage patterns like compound components atm.

feledori avatar Feb 28 '23 17:02 feledori

The best way I found to work around this issue is exporting all components separately and creating a compound component and exporting that as well. Then use separate exports in the the RSC and the compound in RCC.

Example:

const Popover = Object.assign(PopoverRoot, {
  Content: PopoverContent,
  Trigger: PopoverTrigger,
});

export default Popover;
export { PopoverRoot, PopoverTrigger, PopoverContent };

feledori avatar Mar 03 '23 10:03 feledori

@feledori

Also having this problem with compound components I prefer the method of using Object.assign and that doesn't work.

This no longer works (it seems that Next doesn't want us to dot into Client Components in RSC).

Then use separate exports in the the RSC and the compound in RCC.

I think at that point you should just use separate exports, but that's just my opinion :D

Perhaps you can use this pattern:

// _A.tsx
"use client";
export interface AProps {
    foo: string;
}

export const A = (props: AProps) => <p>{props.foo}</p>;

export const AB = () => <p>Hi!</p>;

// A.tsx
import { AB, A as AImpl, type AProps } from "./_A";

const _A = (props: AProps) => <AImpl {...props} />;

export const A = Object.assign(_A, {
  B: AB,
});

// page.tsx
import { A } from "./A";

export default function Page() {
  return (
    <div>
      <A foo="ehe" />
      <A.B />
    </div>
  );
}

DuCanhGH avatar Mar 03 '23 10:03 DuCanhGH

components/Foo/index.ts

export * as Foo from "./foo.ts"

components/Foo/foo.ts

export * from "./Bar.ts"

This allows good "navigate to" and refactoring in IDEs and also better tree shaking as it purely uses ES style exports and not building "objects". Imports like "import {Foo} from "~/app.src/components/foo" <- import autocomplete exists for the namespace. Then usage "Foo.Bar".

Now If the Foo module has both client and RSC components, putting a "use client" at the top is a bad idea. But also working with "client.ts" makes things more complicated and cluttered than needed.

Sometimes, you also need the method mentioned by @feledori. For example when you have some root component and composition components to be used within it.

akomm avatar Mar 20 '23 12:03 akomm

@coderlfm you can add a client.ts or a client folder with a index.ts and put all the files that are Client Components in it just like how I currently do it. I don't think this issue will be fixed anyway.

Is there some fundamental design issue why you think it won't be fixed?

akomm avatar Mar 20 '23 16:03 akomm

@akomm this issue has been open for months. There was also a new issue (#46293) and nothing came out of it either after a month, so yeah.

DuCanhGH avatar Mar 20 '23 16:03 DuCanhGH

@DuCanhGH

I don't know how others see that, but for me the app directory brought a huge improvement in the way how I can organize code. This issue though gives a huge blow to it. Not because its not fixable (the workaround with extra export files for client, etc.) issue, but because the fix breaks the entire organization outside the app directory. I don't want to reduce the new system to just the organization of code, but for me (and evtl. others) its an important part of it.

But that is just by humble perception of the features. I don't know what others think or the devs think others see as important. So this is why I asked if the problem is something fundamental, or maybe its just not a high priority issue. A huge issue for me might be in general not a huge issue for many. If its just not a high priority, at least it might get fixed some day. But if its fundamental it would make me a bit sad, but I wouldn't wait and simply adjust to this reality.

akomm avatar Mar 23 '23 09:03 akomm

I agree this issue is quite annoying in a monorepo context.

Thanks for the tips everyone, for now I chose to explicitly reexport everything:

packages/some-workspace/src/index.ts

// working
export * from './SomeServerComponent'

// not working
export * from './SomeClientComponent'

// working
export { SomeClientComponent, type SomeClientComponentProps } from './SomeClientComponent'

I recommend it over the client.ts trick because if the issue is fixed someday, there will be no need to update all the imports made within the apps:

apps/some-app/src/some-component.ts

// will not change in the future
import { SomeClientComponent, SomeServerComponent } from '@myrepo/some-workspace'

hrougier avatar Mar 23 '23 09:03 hrougier