In the last cycle I spent quite some time using Apollo client to interact with our GraphQL service from React app. Here are some gotchas and nice features I learnt.

Typing system in GraphQL

I first got to know how custom scalar types in GraphQL work when pairing with Nathan on submitting data to graphQL.
This discovery started when we were trying to submit data to GraphQL that has Int64 type, and we keep getting expected an Int64 while the data we had was of TypeScript number type. Turned out that Int64 was a custom scala type that our GraphQL microservice defined.

Then we discovered that there are only a limited set of default scalar types in GraphQL:

  • Int
  • Float
  • String
  • Boolean
  • ID (The ID type is serialized in the same way as a String; however, defining it as an ID signifies that it is not intended to be human‐readable.)

And Int64 is our custom scalar, and its serialization/deserialization rule is defined inside our GraphQL microservice.
Inside schema.graphql, these custom scala are defined by mapping to model files:

1
2
3
4
5
"""
Int64 is a 64-bit, signed integer.
GraphQL Int is 32-bit, so isn't suitable for some cases.
"""
scalar Int64 @goModel(model: "github.com/vend/graphter/model.Int64")

I just had a go with what the type really is when I submit a number:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func UnmarshalInt64(v interface{}) (Int64, error) {
switch v := v.(type) {
case string:
i, err := strconv.ParseInt(v, 10, 64)
if err == nil {
return i, nil
}
case Int64:
return v, nil
case int:
return Int64(v), nil
}
fmt.Printf("%v", v)
fmt.Printf("%v", reflect.TypeOf(v))
return 0, Error("expected an Int64")
}

The console output shows that the input v is of type json.Number 🤔 And we did not define how this type unmarshals to Int64 scalar.

Solution

If we need to transfer an Int64, we need to transfer it as a string.
Although here because we do not need this big of a number, we changed the expected input type on GraphQL side to be Int.

Lessons learnt

GraphQL:

  • scalar is the “minimum”/“leaf” type for GraphQL that is used to build up object
  • there is a limited set of default scalar, and the others are customer defined. Keep in mind that we do not have 100% control of how the endpoint is called.
  • When choosing the type for a GraphQL schema, try to choose the simplest scalar data that does the job

GraphQL Client:

  • Do not make assumptions of how the scalar types are used. The custom scalar is available via the GraphiQL docs, good to have a look. e.g.

Use the InMemoryCache feature of Apollo client to save network round trips on CRUD operations

docs

Three options to make apollo update the cache:

1. If there is a field that has a 1:1 relationship to the data, apollo can do the cache update itself

Got this trick from Greg, the keys are:

  1. to help apollo find out who the keyFields is for this data type:
1
2
3
4
5
6
7
cache: new InMemoryCache({
typePolicies: {
TippingConfig: {
keyFields: ['outletID'],
},
},
}),
  1. make sure the response that updates the data has the correct data structure of this type (i.e. when delete, returning deleted: true is not good enough, need to return the full data structure with status set to OFF)

then whenever there are this data type coming in from a response of a useQuery or useMutation call, apollo knows where to update it.

2. use cache.modify to do a “surgical” update on the data

e.g. To delete a field’s value:

https://www.apollographql.com/docs/react/caching/cache-interaction/#example-invalidating-fields-within-a-cached-object

1
2
3
4
5
6
7
8
cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { INVALIDATE }) {
return INVALIDATE
},
},
})

3. use cache.readQuery and cache.writeQuery to operate on the cache just like we interact with gql server.

Most expressive.
Use standard GraphQL queries.