mattkrick / cashay

:moneybag: Relay for the rest of us :moneybag:
MIT License
453 stars 28 forks source link

Conditionally cached? - part 2 #154

Closed dustinfarris closed 7 years ago

dustinfarris commented 7 years ago

Extending #153

If i have an items index route and items detail route. And I don't know how the user will enter.

This is how I'm attempting to use cache when possible:

Index component:

const itemsQuery = `
query {
  items {
    id
    name
  }
}
`;

const stateToComputed = () => {
  const { data: { items } } = cashay.query(itemsQuery, {
    op: 'items-index'
  });
  return { items };
};

Detail component

const itemQuery = `
query {
  cachedItem @cached(id: $item_id, type: "Item") {
    id
    name
  }
  item(id: $item_id) {
    id
    name
  }
};
`;

const stateToComputed = (state, { item_id }) => {
  const { data: { cachedItem } } = cashay.query(itemQuery, {
    op: 'item-detail',
    key: item_id,
    variables: { item_id }
  });
  return { item: cachedItem };
};

This works.

It uses the cache when the user has already seen the index route. It makes a server request regardless, but the cached data is already rendered.

If the user enters the detail route directly, the server request resolves as normal which fills out the cache and, again, the cache is used to render.

But it feels clunky. Am I missing something?

mattkrick commented 7 years ago

I don't understand what the cachedItem is doing. why not just return the value of item? As a rule of thumb, @cached should only be used in 2 cases:

dustinfarris commented 7 years ago

@mattkrick if i enter the site at the index route, this query hits the server:

items {
  id
  name
}

when i click an item and go to the detail route, this query hits the server:

item(id: $item_id) {
  id
  name
}

but i don't see why that should hit the server when i already have the Item from the "items" query in the index. so i'd like to use the cache there

but, if the user enters the detail route directly via URL, then there is no cache because they never visited the index page. so i actually need to hit the server in that instance.

dustinfarris commented 7 years ago

mattkrick you've said before that cashay will only ask for what it doesn't have, but this doesn't seem to be the case across different Operations, even when the underlying Type is the same.

Item and GraphQLList(Item)

mattkrick commented 7 years ago

you're asking for items and then you're asking for item. How does your cache know that item is a subset of items? Couldn't it be something completely different? We could assume that the unique mix of type & id will always return the same result, but GraphQL doesn't make that assumption, so i'm hesitant to make cashay assume it. In the real world, for the detail view, you'll want extra fields that weren't supplied by items. That means a network request is unavoidable. If you have to make a network request, is the added complexity in your app worth the extra 50 bytes for the name field?

mattkrick commented 7 years ago

that's not to say we can't improve on the pattern. Write a query that makes sense to you, we'll poke holes in it, & then we can make cashay support it.

dustinfarris commented 7 years ago

right, so in my case i'm designing for desktop and mobile, so when the user goes to the detail page, i'd like to have "name" already up there in the header while the network request resolves for everything else. I think there is a clear win for me in leveraging cashay's client cache if I can figure out how to make it work for me.

You're absolutely right that I'm going to want more data in the detail route.

Here's what I'm actually doing (or rather want to do):

Index route:

maps {
  id
  name
}

Detail route

map(id: $map_id) {
  id
  name
  categories {
    id
    name
  }
}

The fields will grow as my app grows, but that's the gist.

Now cashay knows my schema. Cashay should know that the map field on my rootQuery returns a Map type. Cashay should know that the maps field on my rootQuery returns a list of Map type. I'm not sure I follow what you're saying with regard to GraphQL (and Cashay) not assuming that a id/type pair is unique? It seems to me that I've laid that out pretty explicitly in my schema—or maybe this a deeper subject than I'm aware.

Perhaps the item I'm asking for wasn't included in the items result for whatever reason. Well that just means it's not in the cache so we're making a server request for everything after all, right?

dustinfarris commented 7 years ago

So maybe there's two options:

Option 1 Leverage cache implicitly all the time

so assuming I've hit the index route already, in my detail route I do:

cashay.query(mapDetailQuery, variables: { map_id });

cashay sees that it has a record for this id in cache, but not all the subfields requested. so it immediately kicks back what it has:

{
  data: {
    map: {
      id: 123,
      name: "Map Name"
    }
  },
  status: "loading"
}

then it fires off a server request:

{
  query: {
    map(id: $map_id) {
      categories {
        id
        name
      }
    }
  },
  variables: {
    map_id: "123" 
  }
}

after getting the remaining subfields, it combines with existing cache and redux so the stateToComputed receives the full result.

Option 2 something similar with more intervention by the user

Maybe this would expose the cache in a query callback or middleware that the user can return somehow, e.g.

cashay.query(mapDetailQuery, {
  variables: { map_id },
  fromCache: (cashayCache) => {
    let cachedMap = cashayCache.find('Map', map_id);
    if (cachedMap) {    
      return { name: cachedMap.name };
    }
  }
}

and maybe allow the eventual server response to override everything.

dustinfarris commented 7 years ago

Closing in favor of preFetch RFC