HoudiniGraphql / houdini

The disappearing GraphQL client

Home Page:http://www.houdinigraphql.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Embed component references in runtime response?

AlecAivazis opened this issue Β· comments

Ever since the isograph talk at GraphQL Conf, I've been trying to figure out how much of our fragment API we could trim down. I think I've found a pretty good balance between the totally streamlined and unified DX of isograph and staying compliant with the GraphQL spec. The general idea is that components register themselves as a field on a type. something like:

UserAvatar.tsx

type Props = {
  user: GraphQL<`{
      ... on User @componentField(field: "Avatar") {  
        firstName
      }
  }`>
}

export default function UserInfo({ user }: Props) {
  return (
    <div>
      {user.firstName}
    </div>
  )
}

+page.gql

query UserInfo {
  viewer {
    Avatar  # <- does not actually exist in the schema but we would 
            #      generate type extensions and have the field return a `Component` scalar
  }
}

+page.tsx

export default ({ UserInfo }: PageProps) {
  return (
    <div>
      <UserInfo.viewer.Avatar size={10} />
    </div>
  )
}

Behind the scenes, Houdini would import the appropriate file and embed a reference to the component in the query response. This leans even harder into Houdini tightly integrating into your bundler and that feels really good. Also every file has a distinct purpose without any kind of boilerplate or duplication to wire everything up and that feels even better.

This approach forces fragment name unique-ness into a shape that feels slightly easier to swallow but at the cost of potentially conflicting with the schema (although fields and components naturally have opposite casing conventions,but maybe we need another lint rule?).

An important thing to keep in mind is that this API is totally opt-in and would co-exist with the existing useFragment api. I'm not sure this pattern makes sense for one off components like UserDetailsSummaryPane. @componentField pollutes global scope very visibly and probably only makes sense in reusable components like <User.Avatar />. We could go as far as to create some linting rules that enforce that @componentField doesn't show up in src/routes/.

Why this is an improvement:

  • no component import
  • The type definition is the API. This means there's no need to import the type after defining the fragment
  • never have to deal with your prop being different than what you get back from useFragment
  • it's driven by graphql and still spec compliant (we'd generate definitions that extend User to add the Avatar field so IDEs could pick it up)

notes:

  • the parent type "User" is inferred from the inline fragment
  • the prop name is inferred by the analyzing the Props type but it can be explicitly set with the prop argument
  • named exports can be identified with the export argument
  • fragment names don't have to be unique but field names do
  • Named exports can be supported with an argument to @componentField
  • We would extend @arguments to also fall on inline fragments

For completeness, here's what a named export in a vanilla javascript file would look like

graphql(`{
    ... on User @componentField(field: "Avatar", prop: "user", export: "UserInfo") {  
        firstName
    }
})

export function UserInfo({ user }: Props) {
  return (
    <div>
      {user.firstName}
    </div>
  )
}

I've start the work in #1176 and have a working demo for the simplest case πŸš€

This looks so good. πŸŽ‰ πŸŽ‰ πŸŽ‰ πŸŽ‰ I love it. Really clever to use ... on User instead of a (needlessly named) fragment on User. I don't foresee any issues with the design at the moment.

Type inference

  • I think that since you're explicitly defining props, you will correctly get TypeScript inference for e.g. the size prop. Correct?
  • Does the Prop type that the user sees exclude the user prop?

Compilation Errors

  • What if there are multiple @componentField annotations in the same props? Does Houdini fail gracefully in that case?
  • What if you have a legitimate need to pass a second fragment down (e.g. a fragment on viewer for the logged in user's name, and also a specific Foo). If only one of those is a @componentField, will this continue to work?
  • Are field-level merge conflicts reported in a sane way? (I think so... __componentField__User_Avatar is self-documenting)

Interactions with other directives

  • How would @arguments be passed to Avatar?
  • How do you add @defer etc to Avatar's fragment definition?
  • Is there any directive for which it maybe makes sense to apply it to Avatar, but also the fragment? e.g. @include... is there the possibility that you want to include the Avatar but not fetch the fragment; and is that case fully covered by ... on User @include { in Avatar. (It probably is!)
  • Are there any directives that are on fragment definitions, but not on inline fragments, that you want to support, other than @arguments? At Meta, there are "@fb_owner" (or something) directives that fall on fragment definitions, for example.

Neighboring Avatars:

  • I am not confident in Relay's behavior here, but it should refuse to compile something like
... on User {
  foo # a string
}
... on NotUser {
  foo # an int
}

If the foo's types don't align, at least if the inline fragments are being combined into a single object (i.e. we're not generating a discriminated union.). Do we have enough info to ensure that

... on User {
  Avatar
}
... on NotUser {
  Avatar
}

Will generate a coherent type? (I don't know Houdini's behavior here.)

Misc

  • Do component aliases work?
  • What happens if the component alias conflicts with another component? With a field? With a field it selects?
  • Does suspense work as expected, i.e. the component (and not the parent) suspends?
  • Does @required work as expected? Do error boundaries?
  • What about infinite recursion, both direct (Avatar -> Avatar) and indirect (Avatar -> Foo -> Avatar)?
  • Will a VSCode extension suggest the auto-generated fragment? Is it accessible?

Wow thanks for taking the time to type all of that up!! It might be best to move this to discord where we can give each topic a thread but we'll see how far we can get here.

you will correctly get TypeScript inference for e.g. the size prop. Correct?
Does the Prop type that the user sees exclude the user prop?

The type generation still hasn't been added yet but since its totally "faked" (driven by a generated argument passed as a generic arg, not actually inferred from some value) i was planning on just tweaking the support for scalars to handle the Component scalar specially and leaving behind a function that omits the prop name and returns the same value as the passed component.

What if there are multiple @componentField annotations in the same props? Does Houdini fail gracefully in that case?

Right now, it checks for overlaps between all componentFields and the schema to ensure every field only has one canonical definition. The tests for that live here.

What if you have a legitimate need to pass a second fragment down

I think this is a perfect use-case for the ol' useFragment

How would @arguments be passed to Avatar?

Current thinking is that I will translate them to arguments on the field. Ie,

... on User @componentField(field: "Avatar") @arguments(size: { type: "Int"}) { 

}

becomes

type User { 
    Avatar(size: Int)
}

How do you add @defer etc to Avatar's fragment definition?

defer isn't supported at all in the framework so I think we punt on this for now. I imagine it would be able to be applied to the field and be treated as if it was applied to the fragment. They might not be semantically equivalent in all APIs but i'm not sure what else we could do πŸ€”

Equivalence between field and fragments re directives

So this was one of the reasons I wanted to keep the feature hidden behind a feature flag for a bit. I was just going to start by leaving directives in place at start and see if that works for people in the wild.

Neighboring Avatars

This is a good question. If we did want to prevent it, we could generate different scalars for every parent type but I'm not sure its a problem. From a type def generation point, it won't be an issue - every concrete member type of a union is generated independently so we can generate different field types even tho they're the same graphql type.

Do component aliases work?

Hadn't considered that! adding it to the task list now πŸ‘ I think it's definitely something that should be supported.

What happens if the component alias conflicts...

If i'm following the situation correctly, i think it wouldn't validate just like normal graphql fields but I could be mistaken.

Does suspense work as expected

Right now, suspense is mostly driven by the page/layout components (so that the loading states compose correctly). I'm still working on the fragment part of the suspense story but haven't gotten to it. I was working on it in the time up to graphqlconf and then got distracted by your talk in the best way possible πŸ˜…

Does @required work as expected

The @required implementation predates our react support and svelte lacks an error boundary so there are no actions yet but its definitely something i want to add. I'm not sure it really make sense on a component field tho - just like it doesn't make sense for a fragment to have a single value (the reference would be null).

What about infinite recursion

Since the fields are sugar for fragments I dont think this would pass validation

Will a VSCode extension suggest the auto-generated fragment? Is it accessible?

It won't see the __componentField__ fragments but that's by design. It will see the Avatar field since we already generate extensions that get picked up by the lsp.