Skip to content

Remote responses

Petter Eriksson edited this page Dec 6, 2017 · 8 revisions

Other times when we’ve built UI’s that have to communicate with a remote server, each component - usually with the use of a “controller” - would request whatever data they needed. Handling requests this way makes it very easy to do any request and handle any type of response. When using om.next the UI component doesn’t know anything about the remote server. It can only do remote requests by transacting om.next mutations, which is expressed with a data structure. Each mutation in om.next declare whether it should go remote or not (and which remote it should go to), so the remote communication is abstracted away from the UI, making it non-obvious how to handle responses for a specific mutations for a specific UI component.

To get back the power of being able to wait for responses synchronously, we tag each action with an ID and we associate the response from the server with this ID. We've created a namespace for using these messages this which is eponai.client.parser.message.

This fine grained control of mutation responses is usually not needed, as the component's query is describing what it needs from the app-state/database. This level of control is needed when the UI needs to await data from the server and possibly block user interactions, until the UI can progress to the state.

Using messages from a UI component

We'll show you how we use the eponai.client.parser.message namespace from the UI.

In this example we'll create a UI component that:

  1. Can trigger mutations
  2. Display that the mutation hasn't done a round trip yet.
  3. Display the success or error message.
(ns eponai.web.ui.hello-messages
  (:require
    [om.next :as om :refer [defui]]
    [om.dom :as dom]
    [eponai.client.parser.message :as msg]))

(defui HelloMessages
  static om/IQuery
  (query [this]
    ;; UI components that use messages need to have this key to be re-rendered 
    ;; whenever messages are received.
    [:query/messages])
  Object
  (render [this]
    (dom/div nil
      ;; When clicking this button, we'll do an om transact! to greet the server.
      ;; msg/om-transact! transact the query and store the ID for this mutation 
      ;; in the component, so we can look it up later.
      (dom/button 
        {:onClick #(msg/om-transact! this 
                      `[(server/greet {:client-message "Hello server"})])}
        "Send greeting to server!")
      ;; Here we're getting the latest response we've gotten from the server for 
      ;; a specific action in this case 'server/greet
      (let [response (msg/last-message this 'server/greet)]
        ;; If we've never done an action, there's no response. (Responses can 
        ;; also be cleared if needed).
        (when (some? response)
          (dom/div nil
            ;; We can check if the response has been sent but not yet received.
            (dom/span nil (if (msg/pending? response) 
                            "Awaiting response..."
                            "Server responded with: "))
            ;; If we've received the response (it is finalized), we can check if 
            ;; it was a success or if an error occurred.
            (dom/span nil (if (msg/final? response)
                            (if (msg/success? response)
            ;; We get the message from the response by calling the message 
            ;; function. We accept any responses that can be sent via cognitect's
            ;; transit library. Which that we can return arbitrary data in our 
            ;; responses which is really nice.
                              (dom/span nil (str (msg/message response)))
                              (dom/span nil (str "Error: " (msg/message response))))))))))))

Implementation notes

For more information about the implementation, see:

UI API: eponai.client.parser.message

Server parser: eponai.common.parser

The parser contains a multi-method for defining success or error messages for each mutation. The multi-method is called after each return of a mutate function with the same arguments as the mutate function, with the addition of what the mutate returned or the exception it threw if there was an error.

We've added some sugar to our server reads and mutates. In that sugar you can see :success and :error keys being defined for mutations. Those two keys corresponds to what will be sent to the client.

Check out the server mutate namespace if you want to see how we're defining these success and error responses:

Server usage: eponai.server.parser.mutate

Here's how we call the server message multi-method in a parser middleware:

Server parser middleware: eponai.common.parser