When we create our Artemis client we usually pass a :store
option. The store
is a local cache of the results of GraphQL queries. Depending on the fetch
policy we set, Artemis might try to use the cache to resolve the data being
queried. Let's look at what a store is.
The GQLStore
protocol is an abstraction defining a local store that we can
locally execute operations against. Each store implements -read
,-write
,
-read-fragment
, and -write-fragment
functions, which are, respectively,
called by artemis.core/read
, artemis.core/write
, artemis.core/read
, and
artemis.core/write
. Our client will call each of these functions on the store
at specific times, expecting that the store has correctly implemented them.
When we define our store, the -read
implementation will be called with the
parsed GraphQL document, the variables map, and a boolean return-partial?
as
arguments. Return partial is specified at query-time by the executing code and
states whether or a partially fulfilled query (i.e. a query that might return
values for only some fields instead of all) is accepted.
The -read
implementation should use these three arguments to attempt to
fulfill a query (either a fully or partially) and return the result. If the
store isn't able to fulfill the query, it should return nil
.
The -write
implementation is responsible for taking some data and writing it
to a local cache, then returning the updated store.
-read-fragment
and -write-fragment
work pretty much the same way, but also
take an argument that is a reference to a particular node we want to update.
What that reference looks like is up to the store's implementation.
As long as you implement these four functions you can build any kind of store you'd like -- DataScript, IndexedDB, Local Storage, whatever you want -- Artemis will call those functions with the right information at the right times.
Check out the API docs for more on creating your own store.
While you can build your own GQLStore
, it can potentially be complicated to
implement, so Artemis comes with a default store built atop
Mapgraph.
The default store presents a normalized, in-memory database of linked entities. All entities are flatly-stored based on a reference value; nested entities are replaced with a reference lookup.
We can specify the reference values for our entities by passing a :id-fn
when
creating our store. For example, to use each entity's :id
as the lookup we
can do:
(create-store :id-fn (fn [entity] (:id entity)))
It's important to note that lookups should be unique across entities, so make
sure you're id-fn
returns a value that is unique to the entity it's passed.
One approach is to use UIDs. It's also common to prefix the ID with the
entities __typename
value. Here's an example of the second approach:
(create-store :id-fn (fn [entity] (str (:__typename entity) (:id entity))))
The default :id-fn
is :id
, so if the ID value is unique across all of your
entities, you may not even need to specify an :id-fn
.
Because our entities are normalized, we often times get correct cache updates for free after we execute queries and mutations. Let’s say we perform the following query:
{
post(id: 1) {
id
score
}
}
Then we execute a mutation:
mutation {
upvotePost(id: 1) {
id
score
}
}
The ID value on both results matches up, so the score field will automatically be updated across our entire UI.
In some cases, a query requests data that already exists in the store under a different key. A very common example of this is when your UI has a list view and a detail view that both use the same data. The list view might run the following query:
{
books {
id
title
abstract
}
}
When a specific book is selected, the detail view displays an individual item using this query:
{
book(id: $id) {
id
title
abstract
}
}
We know that the data is already in the client cache, but because it's been requested as part of a different query, the store doesn't know that. In order to tell the store where to look for the data, we can point it in the right direction using cache redirects.
When creating our store we can supply a map of redirects via the
:cache-redirects
option. Each key in the map is a field name that we want
to redirect whenever the store can't resolve a result, and the value to the key
is a function that returns a the reference we want to be redirected to. The
function will be called with a map that contains the following:
{:store <the client's store>
:parent-entity <the parent of entity for the field we're on>
:variables <the map of GraphQL variables>}
Assuming that our store is normalizing on the :id
field, our book entity
would be stored by ID. With that said, let's take our example above and
implement a cache redirect for the book
node:
(def cache-redirects
{:book (fn [{:keys [variables]}]
(:id variables))})
(create-store :cache-redirects cache-redirects)
What we've done above is tell our store that if we're not able to resolve the
book
field on any query, run the redirect function specified (which returns
the :id
variables we're querying with) and try to query the selection set
for book from that point in the cache.
Sometimes it's useful to display partially available data while waiting for the remaining data to be loaded. For example, you might query for a list of books using:
{
books {
id
title
}
}
If the user clicks on a specific book, you want to get more information for that book:
{
book(id: $id) {
id
title
abstract
author {
id
firstName
lastName
}
}
}
Depending on how your UI is designed, it might be ok to start rendering the title of the book (since it's already been cached) while the remaining information is being fetched.
By default, our store will only return data for a query that it's able to
fulfill entirely. You can, however, pass a :return-partial? true
option
when reading data:
(a/query! client book-doc {:id 1} :return-partial? true)
You can also pass :return-partial?
when using read
and read-fragment
:
(a/read store book-doc {:id 1} :return-partial? true)
(a/read-fragment store book-fragment-doc 1 :return-partial? true)
In some cases you may want to clear your Mapgraph store. You can easily do this
by calling artemis.stores.mapgraph.core/clear
. The clear
function will
clear out all cached entities.
The entities stored in the Mapgraph cache are just regular Clojure data, so we
can easily serialize it by calling pr-str
.
(pr-str (:entities (a/store client)))
If we wanted to store our entities to localStorage everytime our store changes,
for example, we can use pr-str
in combination with the watch-store
function:
(a/watch-store client
(fn [old-store new-store]
(.setItem js/localStorage
"app-entities"
(pr-str (:entities new-store)))))
Then we could hydrate our cache by passing in our entities at bootstrap:
(create-store :entities (or (.getItem js/localStorage "app-entities") {}))
If you're going to persist the cache on every update, it would be wise to
debounce your function. The Google Closure library that comes within ClojureScript
provides a nice option via the goog.async.Debouncer
class.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close