graphql-code-generator icon indicating copy to clipboard operation
graphql-code-generator copied to clipboard

Support for `loader`-functionality from `@remix`/`react-router-dom`

Open takethefake opened this issue 3 years ago • 1 comments

Is your feature request related to a problem? Please describe.

With the Blog Article from Ryan Florence Remixing React Router the loader Pattern from Remix is said to move to [email protected].

I haven't found a way to populate the loader-Property with hooks

Describe the solution you'd like

I'm currently using graphql-codegen with typescript-urql and since we are generating a hook there and no awaitable fetch function we can't use the functionality of loader with Hooks yet?

I was wondering if we might find a way to generate an awaitable fetch function that calls in my context client.query based on the graphql querries which then populates the loader.

If there is any solution for this let me know, I really like to hear your opinion on this.

takethefake avatar Jul 28 '22 18:07 takethefake

I could implement this. The question is what API would be best. I have few ideas

First approach would be a higher order function, loaders would pass params as variables , and actions would pass the formData by default. We would also generate corresponding typed useLoaderData and useLoaderAction hooks.

The biggest issue is formData and params being dictionaries of strings they almost always require extra parsing. We could let user set a parsing function through config, this way they could for example parse and pass request.url.search instead of params.

import { userLoader, editAction, useUserData, useEditAction } from './generated'

export const loader = userLoader()
export const action = editAction()

// optional config
export const loader = userLoader({
  requestPolicy: 'cache-and-network'
})
//with variables
export const loader = userLoader((ctx) => ({
  variables:
  {
    id: Number(ctx.params.userId),
  },
  requestPolicy: 'cache-and-network'
}))

Second approach is much simpler, it requires user to wrap the function, and write more types, the useLoaderData calls would have to be cast (useLoaderData() as LoaderData<typeof loader>) This easily allows user to parse the results, inside the loader: throw 404, redirects etc.

import { userLoader, editAction } from './generated'

export function loader(ctx: LoaderFunctionArgs){
  return userLoader({ userId: ctx.params.userId })
}

export function action(({ request }: ActionFunctionArgs) => {
  const data = await request.formData()
  return editAction(Object.fromEntries(data))
})

I think the first approach is slightly more interesting, it is would be very powerful, with structured form data

<input name='user.id' value='1'>
<input name='user.name' value='new_name'>

(such form would be parsed into json { user: { id:1, name: 'new_name' } } by an external library).

Let me know what do you think, what are the use cases you are interested in.

PatrykWalach avatar Aug 02 '22 13:08 PatrykWalach

I would also be interested in something like this — but I think the only gap that needs filling is action. For loader, you can just use Awaited<ReturnType<typeof loader>> and you’re good to go. (Note that you shouldn’t type loader with LoaderFunction, but instead its arguments with LoaderArgs, in order to be able to use the return type. (In TypeScript 4.9, the satisfies operator will allow to use LoaderFunction as well.))

For action, however, it’s annoying to parse request.formData() for each request. Here, I think the ideal design of autogenerated helpers is as follows:

  • One function per GraphQL document. For example, const data = await sdk.CreateUser(formData), where
    • data is the mutation response
    • const sdk = getSdk(client) (equivalent to getSdk of the typescript-graphql-request plugin)
    • const formData = await request.formData()
  • Each such function expects all of its variables according to the properties of the leaves of the GraphQL input type
    • e.g. for input CreateUserInput = { firstName: String!, lastName: String! }, the autogenerated function sdk.CreateUser would expect formData.get('firstName') and .get('lastName') to be of the right shape, although the GraphQL mutation only has one variable: an object with key input.
    • In other words, input objects would be “flattened”, because this would be the most convenient setup when creating manual <input />s on the client side
    • Which creates the potential for naming collisions
  • In case the incoming data has the wrong shape: throw new Response(`Invalid value for ${name}`, { status: 422 })

For the moment, I’m doing the validation manually. Just writing down my thoughts, in case I might revisit this decision later.

lensbart avatar Nov 15 '22 22:11 lensbart