Mutating
Mutating with Variables
Like with queries, we can pass a map of variables when calling mutate!
:
(def create-user
(parse-document
"mutation createUser($name: String!) {
createUser(name: $name) {
id
name
}
}"))
(a/mutate! client create-user {:name "Art Vandelay"})
Updating the Cache
If you’re using the default Mapgraph store, most of the time your client-side store will automatically update as you run queries and mutations. On some ocassions, however, you might want to update the cache after a mutation is written to the store; for example, deleting and adding items to a list. Artemis provides an :after-write
hook that you can use for these scenarios. The function provided to :after-write
will be called with a single map of information (shape below) and is required to return an updated store:
{:store <the client's store>
:result <the result of a mutation>
:document <the mutation document>
:variables <the map of arguments>
:optimistic? <a boolean specifying if the write to the store was optimistic> }
Let’s use adding an item to a list as an example of how to use the :after-write
hook. We’ll continue with Reagent for our example. Let’s start with some scaffolding:
(ns app.core
(:require-macros [cljs.core.async.macros :refer [go go-loop]])
(:require [artemis.core :as a]
[artemis.document :refer [parse-document]]
[artemis.network-steps.http :as http]
[artemis.stores.mapgraph.core :as mgs]
[reagent.core :as r]))
;;; Set-up
(def graphcool-url "https://api.graph.cool/simple/v1/cjjh9nmy118fs0127i5t71oxe")
(def network-chain (http/create-network-step graphcool-url))
(def store (mgs/create-store))
(def client (a/create-client :network-chain network-chain
:store store))
;;; GraphQL
(def get-users-doc
(parse-document
"query getUsers {
allUsers {
id
name
}
}"))
(def create-user-doc
(parse-document
"mutation createUser($name: String!) {
createUser(name: $name) {
id
name
}
}"))
The above is code we’ve seen before. Let’s now write functions that will allow us to get all of our users and update the list of users to include a new user. We’ll be using the read
and write
functions to do so:
(defn all-users [store]
(-> store
(a/read get-users-doc {})
(get-in [:data :allUsers])
not-empty))
(defn add-user [store new-user]
(let [users (all-users store)]
(a/write store
{:data {:allUsers (conj users new-user)}}
get-users-doc
{})))
Reading and writing to the cache our coverd in more detail in the Direct Cache Access topic.
Above, you’ll notice that we call read and write with the get-users-doc
, that’s because we want to show the list of users queried by get-users-doc
and update that same list whenever we add a new user by conjoining that new user onto the list and writing it as the updated list.
Not every mutation requires manually updating. If you’re updating a single entity, you usually don’t need to do any manual work. The reasons why are explained in more-depth in the section on the local store.
Now, let’s create a function to update our app-state on GraphQL queries and mutations.
(defonce app-state (r/atom {:store (a/store client)}))
(defn execute! [c]
(go-loop []
(when-let [x (<! c)]
(swap! app-state assoc :store (a/store client))
(recur))))
And, finally, our Reagent views:
(defn create-user-input-and-button []
(let [text (r/atom "")]
(fn []
[:form {:on-submit (fn [event]
(.preventDefault event)
(execute!
(a/mutate! client
create-user-doc
{:name @text}
:after-write (fn [{:keys [store result]}]
(add-user store (get-in result [:data :createUser])))))
(reset! text ""))}
[:input {:type :text
:value @text
:on-change (fn [event]
(reset! text (.. event -target -value)))}]
[:input {:type :submit
:value "Create"}]])))
(defn app []
(when-let [users (all-users (:store @app-state))]
[:div
[:ul
(for [user users]
[:li {:key (:id user)}
(:name user)])]
[create-user-input-and-button]]))
;;; Main
(defn mount []
(r/render [app] (.getElementById js/document "app")))
(defn main ^:export []
(mount))
If you put the code snippets above all together, then run your code, you should see a list of users and a form that allows you to create a new user. Give it a shot; you should see the newly created user added to the bottom of the list.
Optimistic Updates
The above example is great, but there’s a little delay between submitting our form and the new user popping into place. Let’s improve the experience a bit by performing the update optimistically. Optimistically updating refers to a UX pattern whereby you simulate the results of a mutation and update the UI before receiving a response from the server in order to create an application that feels snappier and provides more immediate feedback to the user.
In order to optimistically update our application on a mutation, we specify an :optimistic-result
when calling mutate!
. Here’s our updated submit function:
(fn [event]
(let [temp-id "temp-id"
user-name @text]
(.preventDefault event)
(execute!
(a/mutate! client
create-user-doc
{:name user-name}
:optimistic-result {:data {:createUser {:id temp-id
:name user-name}}}
:after-write (fn [{:keys [store result optimistic?] :as args}]
(let [new-user (get-in result [:data :createUser])]
(if optimistic?
(add-user store new-user)
(replace-user store temp-id new-user))))))
(reset! text "")))
You can see we’ve provided a simulated result for our createUser
mutation. Our :after-write
function gets called on both the optimistic write, and the write that occurs after our server response, so we can use the :optimistic?
flag to, first, add our simulated user (denoted by the temp-id), then replace the optimistic user with the confirmed version.
For reference, here’s what replace-user
might look like:
(defn replace-user [store user-id new-user]
(let [users (map (fn [user] (if (= (:id user) user-id) new-user user))
(all-users store))]
(a/write store
{:data {:allUsers users}}
get-users-doc
{})))