Skip to content

Qlkit FAQ

Conrad Barski edited this page Feb 3, 2018 · 22 revisions

When a Qlkit Transaction is Executed, What Parts of My UI Will Rerender? Will the Server Get Queried Again?

If we run a transaction such as (ql/transact! [:todo/new! {:text "Wash dishes"}]) there are some subtleties in how data is refreshed and how the UI is rerendered. Let's walk through these subtleties:

  1. All immediate client state changes are guaranteed to be rerendered in the UI: Our mutation handler for :todo/new! will likely modify local state. Qlkit will always force a refresh of the entire UI as soon as the mutations are completed to make sure these state changes are all reflected in the UI.
  2. If a transaction could potentially invalidate additional data after being sent to the server, you can request additional server data fetches inside the transaction: For instance, suppose the server for a todo app calculates a special "progress" number that is displayed in the UI and may change in value when a new todo item is added- In that case, we could write our transaction as (ql/transact! [:todo/new! {:text "Wash dishes"}] [:todo/progress]) which essentially says "Send a message to the server to add a new todo item, then tell the server to send back the latest progress." These two query terms will be sent to the server in a single message, but the server will process the individual terms serially: i.e. it will only calculate the progress after the new todo item has been added.
  3. If a transaction is created in the context of a specific component, that component will automatically refetch data from the server as part of the transaction: In the majority of the cases, when a remote mutation requires data to be refetched, that data relates to the qlkit component that triggered the transaction. Because of this, qlkit will automatically enhance any transaction fired from within a specific component to also fetch all data relating to that component. For instance, let's say we have a simple "Counter" component that has a component query of [[:count/value]] and the user clicks on the component to increase the count with the transaction (ql/transact! [:count/increase!]). In that case, qlkit will automatically enhance this transaction as if it was (ql/transact! [:count/increase!] [:count/value])- This means that if the counter value is managed by the server the value will be automatically refetched. In short, this means that that you usually don't need to explicitly refetch data as described in #2.

What is Autoparameterization?

When a qlkit component generates a transaction, the transaction needs to somehow know about the precise identity of the component it originated from: After all, there may be many similar components appearing on a web page. The traditional way to handle this in most web development frameworks is to have a component track its own identity and pass this as an argument for any transaction that is generated.

This approach goes against the philosophy of graph query languages: A query term in a branch of a larger graph query tree should not need to "know" about the full tree. Ideally, it should be entirely insulated from the context in which it exists. Qlkit's autoparamaterization is a mechanism for decorating the output of a component to be tagged with the larger context it exist within. And, by design, qlkit components have only one way to output information: Via transactions.

Here are the steps of the autoparameterization process:

Step 1: Read Parsers Add Environment Information

In a read parser, before executing child parsers, a parser may add information to the environment. For instance, a read parser for a widget list may look as follows:

(defmethod read :widgetlist
   [query-term env _]
   (for [id widget-ids]
        (parse-children query-term (assoc env :widget-id id))))

This widget list simply iterates over a list of widget ids and invokes the parser against it's child query terms, once for every widget id in its list; as it does so, it attaches each widget id to the environment before parsing the child query terms.

The child query for :widget can now pull info out of the environmental variable to help it fetch the right data:

(defmethod read :widget
   [query-term {:keys [widget-id] :as env} _]
   (get-widget-stuff widget-id))

Step 2: Components Generate Transactions

For each widget shown in the app, there will be a Widget component, something like this:

(defcomponent Widget
  (query [[:name]])
  (render [{:keys [name] :as atts} state]
    [:div "Name:" 
          name
          [:button {:label "Frombulate"
                    :on-click (fn []
                                (transact! [:frombulate!]))]]))

Through the magic of autoparamaterization, the environmental variables that we used during parsing are invisibly attached to the runtime components as they are created. This will come into play next, when we generate a transaction.

In the code sample above, we can see a button that can be clicked to frombulate the widget. If the user clicks this button, a qlkit transaction is triggered that reads [:frombulate!].

Step 3: Transactions are Decorated With Parameters

Qlkit will take the [:frombulate!] transaction, which is a component-local transaction and automatically convert it to a global transaction, which includes its parents in the UI tree and all environmental variables that were referenced during parsing- In our example, this means the query term is converted to [:widgetlist {widget-id 42} [:frombulate!]]

Knowing the widget-id of the widget that is being frombulated is a critical piece of information for properly handling this mutation. Qlkit automatically attaches this to our transaction's query.

Step 4: Parsers Handle the Appropriate Parameters

Here, again, is our original widgetlist parser that created the environmental variable:

(defmethod read :widget
   [query-term {:keys [widget-id] :as env} _]
   (get-widget-stuff widget-id))

The exact same parser now also becomes responsible for handling the "parameterized" version: Any parser that attaches an environmental variable also needs to check for that same environmental variable is passed in as a parameter.

We therefore need to modify this parser as follows:

(defmethod read :widgetlist
   [[_ params :as query-term] env _]
   (let [{:keys [widget-id]} params]
     (if widget-id
       [(parse-children query-term (assoc env :widget-id widget-id)]
       (for [id widget-ids]
          (parse-children query-term (assoc env :widget-id id))))))

We have now closed the circle: The context that was added to the query as it is parsed has now become available to the parsers that are processing transactions that originate from components that reside in this context!

Does Qlkit Support Automatic Normalization of Client State, Like Om Next?

No, in qlkit you are responsible for performing your own normalization. However, we have expanded the parsing functionality so that it is easier normalize messages returned by the server, with our sync parsing functionality.

Does Qlkit Have a set-query Command for Updating Component Queries at Runtime?

No: You can roughly think of component queries in qlkit like "types" in a statically compiled language: They all have to be declared up front, even if the app never makes use of certain types (because the user never interacts with certain use paths in the app).

In qlkit, there is no way to dynamically change the query for a component after initialization, even if it's children change.

A component needs to anticipate all the possible types of children it may have, and the parsers are free to NOT fetch data if the relevant children don't make sense in the application's context.

The key to building apps in this model is "Keep components stupid, put all the smarts in the parsers" all business rules belong in the parsers, then the parsers hand over heavily-massaged data to "dumb components."

Does Qlkit Support Query Links/Idents, ala Om Next?

No, since qlkit provides a rich mechanism for maintaining query context via autoparamterization, and since query parsers can contain arbitrary code to pull information out of a top-level state context when needed, we decided not to implement this functionality.

If Qlkit Queries are Static, How Do You Do Routing?

We have created an example that has multiple heterogenous tabs with URL routing that should help explain the appropriate approach- The key is that info as to what the "current tab" is should be lifted into the app-state as an application-level concern so that it can be referenced by parsers to optimize rendering and server queries. Then, you "put the brains in the parsers" so they only populate the parts of the query that are needed at any time. This is identical to the approach outlined here in the section "Routing with Union Queries": https://anmonteiro.com/2016/02/routing-in-om-next-a-catalog-of-approaches/

If Qlkit Queries are Static, How Do I Create a Search Box Component? Wouldn't It Need to Change a Parameter on the Query?

So in our experience it is a bad idea to change the parameter of a component at runtime in these types of graph query systems. This is particularly true if the query is satisfied by the server: You end up in a world of pain if your component parameter updates, and that triggers the client queries, and those trigger the server queries. Roughly, this is because complex data syncing edge cases arise when incorporating the eventual data provided by the server.

Instead you should do the following:

If the query will be satisfied by the Server

You should "lift" the search parameter into the global client state, breaking this into a multi-step process. So the steps are:

  1. The component sends a transaction [:widget/search-param-set! search-string]
  2. The search param is set in the global client state
  3. The remote parser modifies the query to add parameters to it, and the whole thing is sent to the server.

(And the UI component is then rerendered, based on the rules we discussed earlier)

If the query will be satisfied by the Client Application State

You can either use the same technique as for the server (i.e. have a specific global state value to hold the search term, then have a client-side parser for something like :widget/by-search-param which filters the data based on whetever the search term is)

Or, you use can use a brute force approach: Simply have the component fetch all the things and then you can filter them right inside the rendering function (optimally with memoization) which shouldn't be unreasonable, given that the client state is only going to have a limited amount of stuff in it anyway.

Fetch all the things

Note that qlkit definitely assumes that "client compute cycles are cheap"... If this assumption turns out to be wrong in your use case, you may want to look at OmNext, which includes more features to optimize these sorts of scenarios.