Configuring the cache
The Ferry cache normalizes incoming data for your GraphQL operations and stores it using a Store
. This allows the client to respond to future queries for the same data without sending unnecessary network requests.
Data Normalization
Normalization involves the following steps:
- The cache generates a unique ID for every identifiable object included in the response.
- The cache stores the objects by ID in a flat lookup table.
- Whenever an incoming object is stored with the same ID as an existing object, the fields of those objects are merged.
- If the incoming object and the existing object share any fields, the incoming object overwrites the cached values for those fields.
- Fields that appear in only the existing object or only the incoming object are preserved.
Normalization constructs a partial copy of your data graph on your client, in a format that's optimized for reading and updating the graph as your application changes state.
Passing the possibleTypes Map
When the schema contains unions, interfaces queries use fragments, the cache needs a little help
to correctly normalize/denormalize data.
In this case, pass the possibleTypes
parameter to the cache constructor.
This is a Map containing subtype information from the schema.
It is generated by ferry_generator
and located in your schema.gql.dart file.
If you run into DeserializationError
s when using the cache and not passing the possibleTypes map,
this is probably the issue you're running into.
Generating Unique Identifiers
By default, the cache generates a unique identifier using an object's __typename
and any uniquely identifying fields, called keyFields
. By default, the id
or _id
field (whichever is defined) is used as a keyField
, but this behavior can be customized. These two values are separated by a colon (:
).
For example, an object with a __typename
of Task
and an id
of 14
is assigned a default identifier of Task:14
.
Customizing Identifier Generation by Type
If one of your types defines its primary key with a field besides id
or _id
, you can customize how the cache generates unique identifiers for that type. To do so, you define TypePolicy
for the type and include a Map of all your typePolicies
when instantiating the cache.
Include a keyFields
field in relevant TypePolicy
objects, like so:
This example shows three typePolicies
: one for a Product
type, one for a Person
type, and one for a Book
type. Each TypePolicy
's keyFields
array defines which fields on the type together represent the type's primary key.
The Book
type above uses a subfield as part of its primary key. The Book
's author
field must be an object that includes a name
field for this to be valid.
In the example above, the resulting identifier string for a Book
object has the following structure:
An object's primary key fields are always listed alphabetically to ensure uniqueness.
Note that these keyFields
strings always refer to the actual field names as defined in your schema, meaning the ID computation is not sensitive to field aliases.
Disabling Normalization
You can instruct Ferry not to normalize objects of a certain type. This can be useful for metrics and other transient data that's identified by a timestamp and never receives updates.
To disable normalization for a type, define a TypePolicy
for the type (as shown in Customizing Identifier Generation by Type) and set the policy's keyFields
field to an empty Map
.
Objects that are not normalized are instead embedded within their parent object in the cache. You can't access these objects directly, but you can access them via their parent.
TypePolicy
Fields
To customize how the cache interacts with specific types in your schema, you can provide an object mapping __typename
strings to TypePolicy
objects when you create a new Cache
.
A TypePolicy
object can include the following fields:
Overriding Root Operation Types (Uncommon)
In addition to keyFields
, a TypePolicy
can indicate that it represents the root query, mutation, or subscription type by setting queryType
, mutationType
, or subscriptionType
as true
:
The cache normally obtains __typename
information by adding the __typename
field to every GraphQL operation selection set it sends to the server. The __typename
of the root query or mutation is almost always simply "Query"
or "Mutation"
, so the cache assumes those common defaults unless instructed otherwise in a TypePolicy
.
fields
Property
The The final property within TypePolicy
is the fields
property, which is a map from string field names to FieldPolicy
objects.
This API is heavily inspired by Apollo.
The 'merge' function is useful for paginated queries, where you want to merge the results of a query with the results of a previous query also in the cache.
Here is an example of how this could be used:
Let's assume we have a query for reviews, which returns a list of reviews.
This is a query has two arguments, limit
and offset
, which are used to paginate the results.
We can use the merge
function to merge the results of the query with the results of any previous queries.
This shows a custom merge
function for the reviews
query.
First, the keyArgs are set to an empty list, meaning that every query for reviews is cached together.
Usually, every query would be cached separately (e.g reviews(limit: 10, offset: 0)
and reviews(limit: 10, offset: 10)
would be cached separately.
Setting the keyArgs to an empty list means that the queries are cached.
Then, the merge
function is defined, which takes the existing reviews and the incoming reviews and merges them together.
Here, we use a LinkedHashSet
to merge the reviews, which is a set that preserves the order of the elements, but does not allow duplicates.
This is useful because the reviews are paginated, so we want to preserve the order of the reviews, but we don't want to show the same review twice if
one would be returned in multiple pages.
Now the cache merge all the pages in a single list of all reviews.
For more advanced use cases, you can check out the documentation of Apollo for merging and pagination: Apollo.
Ferry's API is very similar.