apollo-feature-requests icon indicating copy to clipboard operation
apollo-feature-requests copied to clipboard

Any plans on adding useFragment() hook and/or subscribing to a fragment change?

Open dko-slapdash opened this issue 5 years ago • 13 comments

Hi.

There is useQuery(), but there is no useFragment() which would allow to watch some fragment in the cache and re-render accordingly.

  • This hook useFragment is supported in Relay: https://github.com/relay-tools/relay-hooks/blob/master/useFragment.md
  • Some people implement work-arounds to achieve the same using hacks with client-only resolvers: https://github.com/abhiaiyer91/apollo-fragment

An example use-case would be the following:

const UserFragment = gql`
  fragment UserFragment on User {
    id
    name
  }
`;

const userFragment = gql`
  query User($id: ID!) {
    viewer {
      user(id: $id) {
        ...UserFragment
      }
    }
  }
  ${UserFragment}
`;

function MyComponent({ id }) {
  // If the user is already somewhere in the cache (most likely it is!),
  // return its data immediately.
  const { data } = useFragment(UserFragment, id);
  // In case the user is not in the cache yet, query it explicitly by ID
  // from some parametrized field.
  useQuery(userFragment, { id });
  // Render the user's name fetched by useFragment.
  return <div>{data?.name}</div>;
  // Next time if the user's name changes in the cache, useFragment will
  // trigger re-rendering again.
}

dko-slapdash avatar Dec 15 '19 07:12 dko-slapdash

I think this would be really useful to help with data masking

wgottschalk avatar Jan 24 '20 02:01 wgottschalk

could this be the same as (rough code):

const useFragment = (fragment, id) => useApolloClient().readFragment({ id, fragment })

?

rajington avatar Feb 10 '20 19:02 rajington

@rajington it's close except readFragment doesn't return an Observable so the result won't update when the store data changes.

dminkovsky avatar Jan 21 '21 15:01 dminkovsky

Is there any solution to observe a fragment change in the cache?

itdhsc avatar Aug 24 '21 08:08 itdhsc

Here's our tracking issue for the implementation of useFragment in Apollo Client v3.5 (the next minor version).

Is there any solution to observe a fragment change in the cache?

To answer @itdhsc's question, yes, but useEffect complicates things, so it's better to keep the details hidden behind an abstraction like useFragment.

I should have a PR open soon using my useFragment-hook branch.

benjamn avatar Aug 24 '21 14:08 benjamn

I was playing around with an implementation earlier this week here: https://github.com/NerdWalletOSS/apollo-cache-policies/pull/33 for useFragment and a collection filter useFragmentWhere. If folks have feedback/suggestions feel free to take a look

danReynolds avatar Aug 26 '21 23:08 danReynolds

Why not to add useEntity also?

const entity = useEntity(User:${id})

gen1lee avatar Feb 21 '22 17:02 gen1lee

Why not to add useEntity also?

const entity = useEntity(User:${id})

I think because you still need to specify a fragment to indicate which fields you want to read. For performance it's inefficient to return all fields by default. Then if you need to pass a fragment anyways, you can just use useFragment.

danReynolds avatar Feb 21 '22 18:02 danReynolds

@danReynolds for performance it is much more inefficient to map entity into fragment, I guess problem here could be with proper typing in typescript.

gen1lee avatar Feb 25 '22 10:02 gen1lee

@danReynolds for performance it is much more inefficient to map entity into fragment, I guess problem here could be with proper typing in typescript.

I was thinking of performance related to the reactive changes. If you return the entire object, Apollo will think you're dependent on all of those fields so if any of them change, your component or other places where you're subscribed to that object will update.

The reactive part of Apollo client is built around invalidation based on field dependencies

danReynolds avatar Feb 25 '22 14:02 danReynolds

I am definitely very interested in this API at the apolloCache layer which is necessary for this to work at the React hook layer.

We have a bunch of cases (ie regulatory requirements) where we want to fire analytics / auditing events when entities change in the cache. This would be a super powerful pattern.

Either being able to watchFragment similar to watchQuery or something like this:

apollo.watchTypes({
  // __typename
  Trip: {
    onAdd: (trip) => {
      // ... do something
    },
    onUpdate: (oldTrip, newTrip) => {
      // ... do something
    },
    onDelete: (trip) => {
      // ... do something
    },
  }
})

clayne11 avatar Jun 18 '22 15:06 clayne11

I am definitely very interested in this API at the apolloCache layer which is necessary for this to work at the React hook layer.

We have a bunch of cases (ie regulatory requirements) where we want to fire analytics / auditing events when entities change in the cache. This would be a super powerful pattern.

Either being able to watchFragment similar to watchQuery or something like this:


apollo.watchTypes({

  // __typename

  Trip: {

    onAdd: (trip) => {

      // ... do something

    },

    onUpdate: (oldTrip, newTrip) => {

      // ... do something

    },

    onDelete: (trip) => {

      // ... do something

    },

  }

})

Yea that's what I made https://github.com/NerdWalletOSS/apollo-cache-policies for a while back initially.

danReynolds avatar Jun 18 '22 19:06 danReynolds

@danReynolds that looks interesting and solves some of my goals, but not all of them. Also would love to see this on native as well.

clayne11 avatar Jun 19 '22 16:06 clayne11

Question about useFragment(), is it supposed to support adding the fragments in our queries?

ie, child component with a fragment written in it:

export const DriverCardFragment = gql`
    fragment DriverCardFragment on Driver {
        id
        firstName
        lastName
        driverName
        createdAt
        updatedAt
    }
`;

I'd like to use that fragment in the query itself, so that I can keep the data logic scoped to the child component and not have to worry about updating parent components or affecting other children, like so:

const DRIVER_GRID_QUERY = gql`
  query QueryDrivers() {
    drivers: queryDrivers() {
     /// fields
      nodes {
        id
        ...DriverCardFragment
      }     
    }
  }
`;

If I write this query and fragment in Apollo studio, it works just fine, but if I try and send the same query with the spread fragment in my app using Apollo Client, I get an error:

"The specified fragment DriverCardFragment does not exist."

The hook portion of the child component, in case I'm doing something wrong:

  const { complete, data } = useFragment_experimental({
    fragment: DriverCardFragment,
    fragmentName: 'DriverCardFragment',
    from: {
      __typename: 'Driver',
      id: driverId,
    },
    returnPartialData: true,
    optimistic: false,
  });

Dmo16 avatar Mar 11 '23 08:03 Dmo16

@Dmo16 as of now, there is no automatic registration of that fragment, so you'd need to import it and include in the query like you normally would when not using useFragment.

import { DriverCardFragment } from './path/to/fragment'

const DRIVER_GRID_QUERY = gql`
  query QueryDrivers() {
    drivers: queryDrivers() {
      # fields
      nodes {
        id
        ...DriverCardFragment
      }     
    }
  }

  ${DriverCardFragment} # <-- Ensure this is added to the query
`;

That's the missing piece in your code right now to make it work. Hope this helps!

jerelmiller avatar Mar 13 '23 17:03 jerelmiller

I just found out about microsoft's apollo-react-relay-duct-tape package. Looks promising but I haven't tried it. Definitely posting it here as it might help some of us with this.

https://microsoft.github.io/graphitation/docs/apollo-react-relay-duct-tape/use-lazy-load-query

https://github.com/microsoft/graphitation

Anyone had any success with it?

TSMMark avatar Mar 13 '23 21:03 TSMMark