I have got an Angular 7 app that accesses data from a GraphQL server.
I directly implemented the example from the docs:
https://www.apollographql.com/docs/react/advanced/caching.html#automatic-updates
This is the service that get the post object and executes the upvote mutation.
export class PostService {
constructor(private apollo: Apollo) { }
public getPost() {
return this.apollo.query({
query: gql`query getPost($id: String!) {
post(id: $id) {
id
score
}
}`,
variables: { id: '123' }
});
}
public upvote() {
return this.apollo.mutate({
mutation: gql`mutation upvote($id: String!) {
upvotePost(id: $id) {
id
score
}
}`,
variables: { id: '123' }
});
}
}
in my component.ts file
public post = this.postService.getPost();
public vote() {
this.postService.upvote().subscribe(console.log);
}
in my component.html
<input type="text" [value]="(post | async).data.post.score" />
<button class="button" (click)="vote()"></button>
The value in the box does not change.
If I add an additional button that calls this function
public updateView() {
post = this.postService.getPost();
}
The gui will update without querying the server, so obviously from cache.
According to the spec this manual refresh step is not supposed to be necessary.
If the id field on both results matches up, then the score field everywhere in our UI will be updated automatically!
Versions of my packages:
apollo-angular: 1.5.0
apollo-angular-link-http: 1.4.0
apollo-angular-link-http-common: 1.4.0
apollo-cache: 1.1.25
apollo-cache-inmemory: 1.4.2
apollo-client: 2.4.12
What do I need to change so the result will actually be updated in the observable that was returned by the original request?
Or do I just not understand the intended mechanisms?
A bit of a wild guess here, but I'm going to suggest this is due to the fact that you're not writing inline Queries; the documentation is going to differ somewhat. Take a look at the refetchQueries prop that you can pass to your Mutation, i.e.
public upvote() {
return this.apollo.mutate({
mutation: gql`mutation upvote($id: String!) {
upvotePost(id: $id) {
id
score
}
}`,
variables: { id: '123' },
refetchQueries: [],
});
When your mutation executes, you're telling GraphQL that you want to execute the queries that you provide in that refetchQueries array. As a result of this, the cache will update. However, I'm not 100% convinced that your query will execute because you're not technically subscribed to it, all you've done is execute a one-time promise to fetch the data from the GraphQL endpoint.
Now that I used the angular part of the docu instead of the React part, I found the solution. (#tombraider Thank you for pushing me in the right direction.)
Instead of query, I need to use watchQuery.
public getPost(id: string) {
return this.apollo.watchQuery({
query: gql`query getPost($id: String!) {
post(id: $id) {
id
score
}
}`,
variables: { id }
}).valueChanges;
}
This changed version of the service-function keeps updateing the GUI.
Related
I am trying to connect the below graphql query with nuxtjs.
query getContent($slug: String!) {
contents (filters: { slug: { eq: $slug } }) {
data {
id
attributes {
title
content {
__typename
... on ComponentContentParagraph {
content
}
}
}
}
}
}
I am getting the following error and not getting the result from the query.
You are using the simple (heuristic) fragment matcher, but your queries contain union or interface types. Apollo Client will not be able to accurately map fragments. To make this error go away, use the `IntrospectionFragmentMatcher` as described in the docs: https://www.apollographql.com/docs/react/advanced/fragments.html#fragment-matcher
I have checked the questions and answers available here.
Apollo+GraphQL - Heuristic Fragment Manual Matching
I have followed the docs from apollo as well.
https://www.apollographql.com/docs/react/data/fragments/#fragment-matcher
I managed to generate possibleTypes as mentioned in the docs.
Here is my next config.
apollo: {
includeNodeModules: true,
clientConfigs: {
default: "~/graphql/default.js",
},
},
This is the default.js
import { InMemoryCache } from "apollo-cache-inmemory";
import possibleTypes from "./possibleTypes.json";
export default () => {
return {
httpEndpoint: process.env.BACKEND_URL || "http://localhost:1337/graphql",
cache: new InMemoryCache({ possibleTypes }),
};
};
I am using strapi for the backend and this query works fine when running from graphql interface.
In my application I am searching for products, then clicking into a product to see more detail about it.
I perform a GraphQL query on each page. The SEARCH query returns type [Product], and the PRODUCT query returns type Product.
// Search page
const SEARCH = gql`
query Search($query: String!) {
searchResults: search(query: $query) {
id
name
images
price
}
}
`
// ProductDetail page
const PRODUCT = gql`
query Product($id: Int!) {
product(id: $id) {
id
name
images
optionSetName
options {
id
images
name
}
price
}
}
`
I have enabled returnPartialData on the PRODUCT query, as some of the fields for that product already exist in the cache from the SEARCH query, and I would like to access them before the server request returns.
I thought I would also have to apply a field policy to reference the pre-existing Product, as I don't know how PRODUCT even knows what its return type is.
However, when I do the following:
const { loading, data: { product } = {} } = useQuery(
PRODUCT,
{ variables: { id: productId, isShallow }, returnPartialData: true }
)
console.log(product)
the following is logged to console (the first is from returnPartialData, the second from server):
Somehow the PRODUCT query has associated itself with the existing Product, without me explicitly writing a cache redirect.
I'm confused how this has occurred? It seems like Apollo must have a reference to the GraphQL schema, and has seen the return type of PRODUCT is Product, then automatically used the id arg to reference the existing product.
Using "#apollo/client": "^3.4.1"
Wow, turns out I had made a field policy ages ago and forgotten about it... xD
typePolicies: {
Query: {
fields: {
product: {
read (_, { args, toReference }) {
return toReference({
__typename: 'Product',
id: args.id
})
}
}
}
}
}
I have a nested component in my app.
At the top of the page, I have a query like
const REPOSITORY_PAGE_QUERY = gql`
query RepositoryPageQuery($name: String!, $owner: String!) {
repository(name: $name, owner: $owner) {
...RepositoryDetailsFragment
}
}
${REPOSITORY_DETAILS_FRAGMENT}
`;
RepositoryDetailsFragment then includes
// list of branches
refs(first: 2, refPrefix: "refs/heads/") {
...BranchesFragment
}
and finally
fragment BranchesFragment on RefConnection {
totalCount
pageInfo {
...PageInfoFragment
}
edges {
node {
id
name
}
}
}
${PAGE_INFO_FRAGMENT}
Obviously, I am not happy, because I need to pass BranchesFragment info around 3 levels deep.
Instead, it would be great if I could read it from the cache directly in my BranchesList component.
I tried to use
client.cache.readFragment({
fragment: BRANCHES_FRAGMENT,
fragmentName: "BranchesFragment"
});
But the problem is that this fragment does not have any id. Is there any way to deal with it and get the fragment info?
Alright, I suddenly came to the solution. Maybe it could be useful for others.
Imagine we have a hierarchy of query -> fragments and components -> subcomponents like this:
RootPageComponent
query
query RepositoryPageQuery(
$name: String!
$owner: String!
$count: Int!
$branchSearchStr: String!
) {
repository(name: $name, owner: $owner) {
...RepositoryDetailsFragment
}
}
${REPOSITORY_DETAILS_FRAGMENT}
component returns the following
<RepositoryDetails repository={data.repository} />
RepositoryDetails
Has a fragment
fragment RepositoryDetailsFragment on Repository {
name
descriptionHTML
defaultBranchRef {
id
name
}
# the branches repository has
refs(first: $count, refPrefix: "refs/heads/", query: $branchSearchStr) {
...BranchesFragment
}
}
${BRANCHES_FRAGMENT}
and returns <BranchesList /> component.
So, instead of passing branch.info from RootPage to RepositoryDetails and then to BranchesList;
You can do the following in BranchesList
const client = useApolloClient();
client.cache.readFragment({
fragment: BRANCHES_FRAGMENT,
fragmentName: "BranchesFragment",
id: "RefConnection:{}" // note this {} - apollow cache adds it when no id is present for the object
})
IMPORTANT!
Make sure to also update type policy for the field and set keyArgs to []
So in this particular case:
RefConnection: {
keyFields: []
...
}
This will give the same result, but you won't have to pass props to nested components and instead can read from cache directly (just like one would do using redux)
I am pretty new to GraphQL. I am using graph.cool in a Vue.js project with apollo.
I am using right now the in-memory cache.
I had previously a simple 'allPosts' query.
And after creating a new one, I used the update() hook and readQuery() + writeQuery()
However I want that logged in users can only see their posts. So I modified the query with a filter.
query userStreams ($ownerId: ID!) {
allStreams(filter: {
owner: {
id: $ownerId
}
}) {
id
name
url
progress
duration
watched
owner {
id
}
}
}
My thought was, that I only need to pass in the userid variable. However this is not working. I am always getting
Error: Can't find field allStreams({"filter":{"owner":{}}}) on object (ROOT_QUERY) undefined.
this.$apollo.mutate({
mutation: CREATE_STREAM,
variables: {
name,
url,
ownerId
},
update: (store, { data: { createStream } }) => {
const data = store.readQuery({
query: USERSTREAMS,
variables: {
id: ownerId
}
})
data.allStreams.push(createStream)
store.writeQuery({
query: USER_STREAMS,
variables: {
id: ownerId
},
data
})
}
})
When you use readQuery or writeQuery, you should use the same variable name. So replace
variables: { id: ownerId }
With
variables: { ownerId }
Also, the reason you are getting an exception is that readQuery throws an exception if the data is not in the store. That happens before the first time you use writeQuery (or get the data with some other query).
You could write some default values to the store before calling this mutation.
You could also use readFragment that returns null instead of throwing an exception. But that would require more changes to your code.
I don't use Relay container, because I'd like to have more control over components. Instead of it I use HOC + Relay.Store.forceFetch, that fetches any given query with variables. So I have the following query:
query {
root {
search(filter: $filter) {
selectors {
_id,
data {
title,
status
}
},
selectorGroups {
_id,
data {
title,
}
}
}
}
}
Then I have to do some mutation on selector type.
export default class ChangeStatusMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation {selectors_status_mutation}`;
}
getVariables() {
return {
id: this.props.id,
status: this.props.status
};
}
getFatQuery() {
return Relay.QL`
fragment on selectors_status_mutationPayload{
result {
data {
status
}
}
}
`;
}
static fragments = {
result: () => Relay.QL`
fragment on selector {
_id,
data {
title,
status
}
}`,
};
getOptimisticResponse() {
return {
result: {
_id: this.props.id,
data: {
status: this.props.status
}
}
};
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
result: this.props.id
},
}];
}
}
Call mutation in component:
const mutation = new ChangeStatusMutation({id, status, result: selector});
Relay.Store.commitUpdate(mutation);
After mutation commitment selector in Relay storage is not changed. I guess that's because of empty Tracked Fragment Query and mutation performs without any fields:
ChangeStatusMutation($input_0:selectors_statusInput!) {
selectors_status_mutation(input:$input_0) {
clientMutationId
}
}
But the modifying selector was already fetched by Relay, and I pass it to the mutation with props. So Relay knows the type, that should be changed, how to find the item and which fields should be replaced. But can not intersect. What's wrong?
So, you're definitely a bit "off the ranch" here by avoiding Relay container, but I think this should still work...
Relay performs the query intersection by looking up the node indicated by your FIELDS_CHANGE config. In this case, your fieldIDs points it at the result node with ID this.props.id.
Are you sure you have a node with that ID in your store? I'm noticing that in your forceFetch query you fetch some kind of alternative _id but not actually fetching id. Relay requires an id field to be present on anything that you later want to refetch or use the declarative mutation API on...
I'd start by checking the query you're sending to fetch whatever this result type is. I don't see you fetching that anywhere in your question description, so I'm just assuming that maybe you aren't fetching that right now?