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

feat: Support GraphQL Fragments with Apollo Client and Fragment Registry #9140

Merged
merged 33 commits into from
Sep 21, 2023

Conversation

dthyresson
Copy link
Contributor

@dthyresson dthyresson commented Sep 7, 2023

This PR adds support to register GraphQL Fragments.

It:

  • adds a fragmentRegistry to the Apollo client cache
  • Sets up graphql config to recognize fragments in VSCode
  • Generates possible types for Apollo client union type and interface support
  • Exposes a useRegisteredFragment to render cached data
  • New test app with fragments for future CI
  • Adds possible types to create redwood app templates
  • Document the possible types code gen
  • Documents use of fragments and the fragment registry helpers

This is a WIP and first phase for easier fragment support.

See the test fixture for some examples and the docs for usage.

Still to code are use in mutations and updating the fragment in the client cache directly.

@dthyresson dthyresson changed the title feat: Draft- Support GraphQL Fragments with Apollo Client with Auto Registration feat: Draft - Support GraphQL Fragments with Apollo Client with Auto Registration Sep 7, 2023
@dthyresson dthyresson marked this pull request as ready for review September 7, 2023 17:36
@dthyresson dthyresson marked this pull request as draft September 7, 2023 17:36
@dthyresson dthyresson self-assigned this Sep 7, 2023
@dthyresson dthyresson added release:feature This PR introduces a new feature topic/graphql labels Sep 7, 2023
@dthyresson dthyresson added this to the next-release milestone Sep 7, 2023
@dthyresson dthyresson changed the title feat: Draft - Support GraphQL Fragments with Apollo Client with Auto Registration feat: Draft - Support GraphQL Fragments Client Sep 12, 2023
Comment on lines 8 to 20
groceries {
... on Fruit {
id
name
isSeedless
ripenessIndicators
}
... on Vegetable {
id
name
vegetableFamily
isPickled
}
Copy link
Contributor

@dustinsgoodman dustinsgoodman Sep 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you're in a draft so maybe too early to comment on this one but I'd expect instead of ... on Fruits { to have a ...fruitsFragment instead that is defined elsewhere like:

const FRUITS_FRAGMENT = gql`
  fragment fruitsFragment on Fruit {
    id
    name
    isSeedless
    ripenessIndicators
  }
`;

This is a valid case for union typing resolution though so super valuable nonetheless but this isn't quite the power of fragments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! That's the plan. This test app was currently just to have the possibleTypes code gen working.

I have a separate app that I am actually testing fragments and then will rework this ci test project.

The hope is we can CI some e2e tests to ensure fragments return and render properly.

It will be something like (but not this exact code)...

import type { Fruit } from 'types/graphql'

import { registerFragment } from '@redwoodjs/web/apollo'

const { useRegisteredFragment } = registerFragment(
  gql`
    fragment Fruit_info on Fruit {
      id
      name
      isSeedless
      ripenessIndicators
    }
  `
)

const Fruit = ({ fruitId }: { fruitId: string }) => {
  const { data: fruit, complete } = useRegisteredFragment<Fruit>(fruitId)

  console.log(fruit)

  return (
    complete && (
      <div>
        <h2 className="font-bold">Fruit Name: {fruit.name}</h2>
      </div>
    )
  )
}

export default Fruit

Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is just beautiful. 10/10

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't talk to the Apollo constraints, but the Relay useFragment API tends to look like this:

const fragment = graphql`
  fragment UserDisplay on User {
    id
    username
    usernameID
    avatarURL
  }
`

export type UserDisplayOptions = {
  noLink?: boolean
  noPopover?: boolean
  size?: "small" | "medium" | "large" | "block"
  mode: "text-only" | "avatar-only" | "text-and-avatar" | "avatar-and-text"
  style?: ViewStyle
  textStyle?: TextStyle
}

export const UserDisplay = (props: { user: UserDisplay$key } & UserDisplayOptions) => {
  const user = useFragment(fragment, props.user)
  if (!user) {
    return null
  }
  return <UserDisplay_RAW {...props} user={user} />
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't talk to the Apollo constraints, but the Relay useFragment API tends to look like this:

Apollo's useFragment is a bit different -- it is new to 3.8.x and specifically pluck data out of the cache based on the cache key (ie reference, ie "typename:your-id-ref").

useFragment represents a lightweight live binding into the Apollo Client Cache and enables Apollo Client to broadcast very specific fragment results to individual components. This hook returns an always-up-to-date view of whatever data the cache currently contains for a given fragment. useFragment never triggers network requests of its own.

See: https://www.apollographql.com/docs/react/data/fragments/#usefragment

Since it's new, most of the time you'd use the registerFragment to add the fragment to the client ... yes, you have to register each fragment with Apollo and yes you have to inform Apollo all the possible types unions might have.

Hence the helpers in tis PR to:

  • generate possible types via code gen
  • add the registry
  • let you use registerFragment or... and then just render the data in a component
  • let you use useRegisterFragment which is this bound useFragment hook that will fetch from the cache and redisplay data it the cache is change on a separate query or a mutation that write to the cache.
  • useRegisterFragment just needs an id ... the reference to the data stored in the cache.

In short, you don't have to use useRegisterFragment -- if you do, just pass an id and then it will grab the data from the cache. Or, After registering it and using the defined fragment in a query, you can just access the data and render it.

Note: Still looking for a nicer way to set possibleTypes than this. But possibleTypes lives in your project, not the fw where the Apollo client is created:

import introspection from 'src/graphql/possibleTypes'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'

import './index.css'

const App = () => (
  <FatalErrorBoundary page={FatalErrorPage}>
    <RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
      <RedwoodApolloProvider
        graphQLClientConfig={{
          cacheConfig: {
            possibleTypes: introspection.possibleTypes,
          },
        }}
      >
        <Routes />
      </RedwoodApolloProvider>
    </RedwoodProvider>
  </FatalErrorBoundary>
)
``

@dthyresson
Copy link
Contributor Author

Here's an update on what the Fruit fragment component can be:

import type { Fruit } from 'types/graphql'

import { registerFragment } from '@redwoodjs/web/apollo'

import Card from 'src/components/Card/Card'
import Stall from 'src/components/Stall'

const { useRegisteredFragment } = registerFragment(
  gql`
    fragment Fruit_info on Fruit {
      id
      name
      isSeedless
      ripenessIndicators
      stall {
        ...Stall_info
      }
    }
  `
)

const Fruit = ({ id }: { id: string }) => {
  const { data: fruit, complete } = useRegisteredFragment<Fruit>(id)

  console.log(fruit)

  return (
    complete && (
      <Card>
        <h2 className="font-bold">Fruit Name: {fruit.name}</h2>
        <p>Seeds? {fruit.isSeedless ? 'Yes' : 'No'}</p>
        <p>Ripeness: {fruit.ripenessIndicators}</p>
        <Stall id={fruit.stall.id} />
      </Card>
    )
  )
}

export default Fruit

With Fruit, Vegetable and Product and Stall Fragment components.

Queried by:

const GET_GROCERIES = gql`
  query GetGroceries {
    groceries {
      ...Fruit_info
      ...Vegetable_info
    }
  }
`

const GET_PRODUCE = gql`
  query GetProduce {
    produce {
      ...Produce_info
    }
  }
`

image

@dthyresson
Copy link
Contributor Author

As a demo of how Apollo's useFragment can pull info from cache, here's an AllGroceriesCell that queries **just the ids **:

import type { AllGroceriesQuery } from 'types/graphql'

import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'

import Fruit from 'src/components/Fruit'
import Vegetable from 'src/components/Vegetable'

export const QUERY = gql`
  query AllGroceriesQuery {
    allGroceries: groceries {
      ... on Fruit {
        id
      }
      ... on Vegetable {
        id
      }
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>Empty</div>

export const Failure = ({ error }: CellFailureProps) => (
  <div style={{ color: 'red' }}>Error: {error?.message}</div>
)

export const Success = ({
  allGroceries,
}: CellSuccessProps<AllGroceriesQuery>) => {
  return (
    <ul>
      {allGroceries.map((item) => {
        return (
          <li key={item.id}>
            <Fruit key={item.id} id={item.id} />
            <Vegetable key={item.id} id={item.id} />
          </li>
        )
      })}
    </ul>
  )
}

But because the id is passed to the fragment component, the full data renders:

image

It only works because the cache was populated by the other useQuery.

@dthyresson
Copy link
Contributor Author

@Josh-Walker-GM Have updated the useCache and documentation per our review yesterday and ready for approval (pending CI).

Copy link
Collaborator

@Josh-Walker-GM Josh-Walker-GM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I walked through creating another app which uses what DT has added here and it all appears to work smoothly. I expect that there will be revisions of based on how people find the DX but I think this is good to merge now.

@dthyresson dthyresson enabled auto-merge (squash) September 21, 2023 13:50
@dthyresson dthyresson merged commit 2cbdf11 into redwoodjs:main Sep 21, 2023
28 checks passed
jtoar pushed a commit that referenced this pull request Oct 8, 2023
…try (#9140)

This PR adds support to register GraphQL Fragments.

It:

* adds a fragmentRegistry to the Apollo client cache
* Sets up graphql config to recognize fragments in VSCode
* Generates possible types for Apollo client union type and interface
support
* Exposes a useRegisteredFragment to render cached data
* New test app with fragments for future CI
* Adds possible types to create redwood app templates
* Document the possible types code gen
* Documents use of fragments and the fragment registry helpers

This is a WIP and first phase for easier fragment support.

See the text fixture for some examples and the docs for usage.

Still to code are use in mutations and updating the fragment in the
client cache directly.
@jtoar jtoar modified the milestones: next-release, v6.4.0 Oct 10, 2023
@jtoar jtoar modified the milestones: v6.4.0, next-release Nov 2, 2023
jtoar added a commit that referenced this pull request Nov 2, 2023
@Tobbe Tobbe modified the milestones: next-release, v6.5.0 Dec 1, 2023
@jtoar jtoar modified the milestones: v6.5.0, next-release Dec 5, 2023
@Tobbe Tobbe modified the milestones: next-release, v6.6.0 Dec 22, 2023
@jtoar jtoar modified the milestones: next-release, v7.0.0 Jan 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fixture-ok Override the test project fixture check release:feature This PR introduces a new feature topic/graphql
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants