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

Local state resolvers with reactive data #5550

Closed
TiuSh opened this issue Nov 5, 2019 · 12 comments
Closed

Local state resolvers with reactive data #5550

TiuSh opened this issue Nov 5, 2019 · 12 comments

Comments

@TiuSh
Copy link

TiuSh commented Nov 5, 2019

Hi 👋

I'm playing around with Apollo's Local State Management feature and I found a strange behaviour that has not been made clear in the documentation. Could you help me understand if this is a bug, or if it's by design and if there's a workaround?

Here it is:

  • I have an Apollo Server with these typeDefs:
  type Query {
    greetings: [Greeting]
  }

  type Greeting {
    word: String
  }
  • the Apollo client is set with a local resolver and a local state:
const client = new ApolloClient({
  // ...
  defaultOptions: {
    query: {
      fetchPolicy: "network-only"
    },
  },
  resolvers: {
    Greeting: {
      name: (parent, args, { client }) => {
        const state = client.readQuery({
          query: gql`
            query StateQuery {
              localState {
                value
              }
            }
          `
        });

        return state.localState.value;
      }
    }
  }
});

client.writeData({
  data: {
    localState: {
      __typename: "LocalState",
      value: "John"
    }
  }
});

With this setup, everything is ok on the first query with react-apollo hook:

const { data } = useQuery(gql`
  query ComponentQuery {
    localState @client {
      value
    }
    greetings {
      name @client(always: true)
      word
    }
  }
`);

console.log(data);
/* {
  "localState": {
    "value": "John",
    "__typename": "LocalState"
  },
  "greetings": [
    {
      "name": "John",
      "word": "Hello",
      "__typename": "Greeting"
    },
    {
      "name": "John",
      "word": "Welcome",
      "__typename": "Greeting"
    }
  ]
} */

But when I update the local state with:

client.writeQuery({
  query: STATE_QUERY,
  data: {
    localState: { __typename: "LocalState", value: "Foo" }
  }
});

The previous query is correctly re-run, but the local field name has not been updated:

console.log(data);
/* {
  "localState": {
    "value": "Foo",
    "__typename": "LocalState"
  },
  "greetings": [
    {
      "name": "John",
      "word": "Hello",
      "__typename": "Greeting"
    },
    {
      "name": "John",
      "word": "Welcome",
      "__typename": "Greeting"
    }
  ]
} */

You can find a Sandbox set up here: https://codesandbox.io/s/apollo-local-state-management-5dy2f?fontsize=14&module=%2Fsrc%2FApolloProvider.tsx

Is it intended? Do you plan on supporting this kind of feature?

Thank you for your help!

@freshollie
Copy link

freshollie commented Nov 8, 2019

The greetings resolver has been cached, so any subsequent queries will not execute the resolver. To fix with behaviour you can turn off the caching policy no-cache which is not recommended.

If you want to update the greetings local query, you have to write the new version of that query into the cache.

I too also didn't understand this, and struggled for days reading the documentation and just trying it out until I found the issue.

Edit: you can use @client(always: true) too.

@benjamn benjamn added this to the Release 3.0 milestone Nov 11, 2019
@dcecile
Copy link

dcecile commented Nov 15, 2019

@freshollie Can give an example of writing a new version of the query into the cache?

Also, it seems like @client(always: true) is already in @TiuSh's example but it has no effect...

@freshollie
Copy link

@dcecile Please see: https://codesandbox.io/s/apollo-local-state-management-zrwel

The writeQuery should not only update the localState, but also update all greetings objects in the cache in order to achieve the desired effect.

The reason the @client(always: true) doesn't work is because the overall greetings query has already been cached, so any subsequent queries are not going to execute the name resolver.

@dcecile
Copy link

dcecile commented Nov 18, 2019

@freshollie OK I understand now, thanks 🙏

I'd be nice if Apollo treated readQuery inside of a local resolver as a "watch query", just like useQuery inside of React component.

My feature request is if the source of readQuery changes and the local resolver is still in use by some React component's useQuery, then the local resolver would automatically re-run and its results get sent to the React component.

This way I wouldn't need to do all of the bookkeeping and updating myself (i.e. which React components are active, which fragments they use, which local resolvers of those fragments are used, and how to recalculate the results of all those local resolvers).

@TiuSh
Copy link
Author

TiuSh commented Nov 18, 2019

Thanks for your help @freshollie!
Playing around with your exemple I now fully understand the issue... But I was expecting the same as @dcecile:

if the source of readQuery changes and the local resolver is still in use by some React component's useQuery, then the local resolver would automatically re-run and its results get sent to the React component.

That would be really nice! And that would finally make Apollo the perfect fit for our local state management!

@dcecile
Copy link

dcecile commented Nov 18, 2019

FYI, @sirctseb posted to StackOverflow with an alternate workaround, to use a top-level watchQuery subscription:

client.watchQuery({ query: sourceQuery }).subscribe(value =>
  client.writeData({ data: { resolvedDevice: doStuffWithTheData(value.data.selectedDevice) } });

(He explains that this solution needs extra bookkeeping, potentially automated using a HOC, to make sure that the watchQuery only runs when it's actually needed by a component.)

@benjamn
Copy link
Member

benjamn commented Jan 14, 2020

In Apollo Client 3.0, a better way to implement this behavior is to us a custom read function, and remove the Greeting entry from your resolvers:

import { InMemoryCache } from "@apollo/client";

const LocalStateValueQuery = gql`
  query StateQuery {
    localState {
      value
    }
  }
`;

const cache = new InMemoryCache({
  typePolicies: {
    Greeting: {
      fields: {
        name(existingName: string) {
          return existingName || cache.readQuery({
            query: LocalStateValueQuery,
          })?.localState?.value;
        },
      },
    },
  },
});

Because the name function reads the LocalStateValueQuery, its result will be automatically invalidated when new data is written for the localState field.

@benjamn
Copy link
Member

benjamn commented Jan 14, 2020

If you want to get a little fancier, you could give your LocalState singleton object an ID, which allows creating a reference to it, so you can read its fields without calling cache.readQuery:

const cache = new InMemoryCache({
  typePolicies: {
    LocalState: {
      // Indicate that the ID of the singleton LocalState object does not depend
      // on any of its fields, and is therefore constant: "LocalState:{}".
      keyFields: [],
    },
    Greeting: {
      fields: {
        name(existingName: string, { readField, toReference }) {
          return existingName || readField("value", toReference({
            __typename: "LocalState",
          }));
        },
      },
    },
  },
});

This pattern is also fully reactive, and should be much faster because it skips the overhead of cache.readQuery.

@benjamn
Copy link
Member

benjamn commented Jan 14, 2020

While I stand by my recommendations above, I'm not entirely happy with either one.

Here's the API I would really like to use:

const cache = new InMemoryCache({
  typePolicies: {
    Greeting: {
      name() {
        return nameVar.get();
      }
    }
  }
});

// A default value can be provided when the variable is created,
// which also allows TypeScript to enforce the type.
const nameVar = cache.variable("John");

// The variable can be updated anywhere, like so:
nameVar.set("Ben");

@benjamn
Copy link
Member

benjamn commented Jan 16, 2020

Ok, here's my take on the reactive local variable idea: #5799

@hwillson
Copy link
Member

#5799 was merged, so I'll close this off. Thanks!

@TiuSh
Copy link
Author

TiuSh commented Jan 21, 2020

Nice! Thanks @benjamn for your work, can't wait to try it!

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 16, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants