orval icon indicating copy to clipboard operation
orval copied to clipboard

SWR: Attempted import error: 'swr' does not contain a default export (imported as 'useSwr').

Open Southclaws opened this issue 1 year ago • 16 comments

What are the steps to reproduce this issue?

  • Run codegen with swr generator
  • Import one of the helper functions into an RSC component
  • Terminal shows this error

Nothing breaks, it's just a warning, but annoying

More info: https://github.com/vercel/swr/issues/2694

What happens?

image

What were you expecting to happen?

The codegen could add "use client" to parts that actually call useSWR and separate keys from this code as the keys are used in server components.

Any other comments?

Obviously we don't want to call useSWR in server components, however there are parts of the generated code that are useful in server components. One of these, which I make use of in Storyden's codebase is the generated keys. This means I can use fetch on the server but still use the generated types and paths.

export const getThreadListKey = (params?: ThreadListParams) =>
  [`/v1/threads`, ...(params ? [params] : [])] as const;

These generated key functions are useful to keep everything type safe - even while Orval doesn't generate server code, I can use these to ensure my code is at least somewhat type safe for server components:

import { server } from "src/api/client";
import { ThreadListOKResponse } from "src/api/openapi/schemas"; // schema imports are fine - no useEffect dependents here
import { getThreadListKey } from "src/api/openapi/threads"; // this causes the error, even though I'm not actually using useEffect dependents (useSWR) - the runtime still warns me because getThreadListKey is in the same file as a useSWR import+call

import { Client } from "./Client";

type Props = {
  category: string;
};

export async function FeedScreen(props: Props) {
  const key = getThreadListKey({ categories: [props.category] })[0]; // imported from Orval

  const data = await server<ThreadListOKResponse>(key);

  return <Client category={props.category} threads={data.threads} />;
}

What versions are you using?

Package Version: "orval": "^6.17.0",

Southclaws avatar Sep 16 '23 11:09 Southclaws

PR is welcome. SSR has been a nightmare for library implementors!

melloware avatar Nov 05 '23 15:11 melloware

I'd be up for doing a PR to split these files, where's a good place to start?

I think we just need to put the SWR keys in a separate file to the actual fetch calls, should be quite simple!

Southclaws avatar Nov 06 '23 12:11 Southclaws

Hmm this was handled differently for React Query and the files need not be split for SSR: https://github.com/anymaniax/orval/issues/985

melloware avatar Nov 06 '23 13:11 melloware

@Southclaws

Hi, this similar issue seems to have been resolved in v14 of Next.js. This may also have been resolved, so could you please check again with v14?

@melloware

This problem is probably a problem with Next.js, not orval. The "bug" label attached to issue may not be necessary.

soartec-lab avatar Dec 30 '23 00:12 soartec-lab

I removed bug and will close for now unless the OP can confirm.

melloware avatar Dec 30 '23 02:12 melloware

Why was this issue closed? I just ran into the same problem with SWR. I imported the function that does Axios request into a server component and wanted to do a request on the server side but it failed with an error that matches the name of this issue. This makes the SWR generator kind of useless for those who want to do both server-side and client-side fetching unless I generate completely separate API files for use in server components but it feels very wrong considering that the files generated for the SWR client already have everything I need to use on server as well. I think splitting the files is the way to go. I don't know how come that this works in ReactQuery, but even there the hooks could be just tree-shaken away from the server bundle.

isoroka-plana avatar Jan 15 '24 22:01 isoroka-plana

I got some extra information: To be more correct this error doesn't crash the build and everything seems to be working on the server, including loading data. Nonetheless, it doesn't seem to be safe to use in production like this.

I tried to split it manually into separate files, but as long as they are re-exported by the same index.ts file, the error will persist. I imagine creating an entirely different barrel file would be quite a big change for orval, but thankfully the file with hooks can be marked with 'use client' directive and the server will ignore it, I can confirm that the error is then gone. So I'd propose to do something like this (in case of split-tags):

openapi/
├─ api/
│  ├─ tag-1/
│  │  ├─ tag-1.ts
│  │  ├─ tag-1-hooks.ts
│  ├─ tag-2/
│  │  ├─ tag-2.ts
│  │  ├─ tag-2-hooks.ts
│  ├─ index.ts
├─ models/
│  ├─ index.ts

With hooks files having 'use client' on top. Something similar can be probably done about split and tags mode, but as for single I am afraid nothing can be done.

isoroka-plana avatar Jan 15 '24 23:01 isoroka-plana

Nice debugging. Still a big change but at least you know what works now.

melloware avatar Jan 15 '24 23:01 melloware

@isoroka-plana

Hi, I would like to ask you a few questions to help me understand better.

  1. Is it correct that what you are looking for is to use only the following two functions that do not depend on swr?
  • key generation function
  • Axios client
  1. Also, is it correct to understand that it is used in the server-side component of Next.js?

soartec-lab avatar Jan 20 '24 15:01 soartec-lab

  1. Yes, I am looking to use axios client, and the function that does the axios request (if that's what you mean by key generation function, there's one that generates swr key, but it's only relevant for SWR itself). Here's an example that I am looking to import:
export const booksGet = (options?: SecondParameter<typeof customInstance>) => {
  return customInstance<Book[]>({url: `api/v1/books`, method: 'GET'}, options);
}
  1. Yes, that's exactly right. Example component:
export const BooksPage = async () => {
  const books = await booksGet();

  return (
   <div>
     {books .map(book => <Book key={book.id} book={book} />)}
   </div>
  )
}

So the axios stuff is client/server agnostic while swr stuff is client only, that's why in order to make convenient to use it's best to split them into separate files. Cause I can create the above function myself using the same axios client and models used by Orval but still that means I have to hardcode the API path.

isoroka-plana avatar Jan 20 '24 15:01 isoroka-plana

@isoroka-plana

Thank you for letting me know. I thought this could be achieved by specifying the axios client, what do you think?

orval.config:

module.exports = {
  'petstore-file': {
    input: {
      target: './petstore.yaml',
    },
    output: {
      client: 'axios',
      target: 'src/gen/endpoints',
    },
  },
};

generated

const listPets = (
    params: ListPetsParams,
    version: number = 1,
 ) => {
      return listPetsMutator<Pets>(
      {url: `/v${version}/pets`, method: 'GET',
        params
    },
      );
    }

usage:

 const pets = await listPets(params);

  return (
   <div>
     {pets .map(pet => <Pet key={pet.id} pet={pet} />)}
   </div>
  )
}

soartec-lab avatar Jan 21 '24 00:01 soartec-lab

@soartec-lab thank you for suggesting this idea. I considered this option but I still need swr for requests from the client side. This would require me to generate 2 different outputs 1 for plain axios and 1 for swr. Each of these will create their own models if I get everything correctly. It's a messier approach than just creating copies of the functions that I need by myself. But from the perspective of Nextjs + orval user there should be a way to use 1 generator and use its output on both sides.

isoroka-plana avatar Jan 21 '24 01:01 isoroka-plana

@isoroka-plana

Hmmm. This can be achieved by creating automatic generation of axios for the server component and automatic generation of swr for the client component. For example, the directory structure is as follows.

# generated by `axios`
gen/server-endponin

# generated by `swr`
gen/client-endponin

But you say you don't want that. In other words, rather than managing two generators, you want to satisfy both with one generator.

Could you please explain in more detail why the above method is insufficient?

soartec-lab avatar Jan 21 '24 01:01 soartec-lab

@soartec-lab Because the models will be generated by each of the generators, it will create a mess with imports since 1 and the same API. And swr client already uses axios and exports the necessary functions.

isoroka-plana avatar Jan 21 '24 01:01 isoroka-plana

I understand. It's true that if you're using both, it's confusing and difficult to use. I change the label to enhancement.

soartec-lab avatar Jan 21 '24 01:01 soartec-lab

I figured out an undocumented feature, that the models can be generated without generating endpoints and the endpoint generators can be pointed to models generated by the model generator 🤷 I found it surprising but it actually works, although there was a bit of a hiccup that the API exports also reexported the models, so I had to disable index files there. I managed to achieve the desired result by using multiple entries in the config file like so:

export default defineConfig({
  myapi: {
    input,
    output: {
      mode: 'tags-split',
      workspace: './build/openapi/myapi',
      schemas: './models',
    },
  },
  myapiClient: {
    input,
    output: {
      mode: 'tags',
      workspace: './build/openapi/myapi',
      target: './api/client',
      schemas: './models',
      indexFiles: false,
      client: 'swr',
      override: {
        mutator: {
          path: '../../../src/orval-mutator/custom-client-instance.ts',
          name: 'customInstance',
        },
      },
    },
  },
  myapiServer: {
    input,
    output: {
      mode: 'tags',
      workspace: './build/openapi/myapi',
      target: './api/server',
      schemas: './models',
      indexFiles: false,
      override: {
        mutator: {
          path: '../../../src/orval-mutator/custom-server-instance.ts',
          name: 'customInstance',
        },
      },
    },
  },
});

and then in tsconfig.json:

"@myapi/api": ["./build/openapi/myapi/index.ts"],
"@myapi/api/client/*": ["./build/openapi/myapi/api/client/*"],
"@myapi/api/server/*": ["./build/openapi/myapi/api/server/*"],

Maybe simply the documentation can be improved to have some sort of article with advanced stuff and maybe no need to complicate the implementation.

isoroka-plana avatar Mar 19 '24 22:03 isoroka-plana

I've found a workaround for this edge case so closing

soartec-lab avatar Aug 07 '24 00:08 soartec-lab