View Source LiveQuery

LiveQuery is a new way to load, access, and manage data in your live views. LiveQuery decouples data loading and storage from rendering.

Traditionally these concerns have been combined. For example, imagine the following component tree:

flowchart TD;
subgraph "live view"
  A --> B & C;
  B --> D & E;
  C --> F;
end

with the following data dependencies:

flowchart TD;
subgraph "live view"
  D
  E
  F
end
subgraph "data store"
  1[(1)]
  2[(2)]
end
D --> 2;
E --> 1 & 2;
F --> 1;

We're generally taught to handle this by fetching data in a common ancestor, like so (dotted lines indicate network hops):

flowchart TD;
subgraph "live view"
  A
  B
  C
  D
  E
  F
end
subgraph "data store"
  1[(1)]
  2[(2)]
end
1 -.-> A;
A --> B & C;
2 -.-> B;
B --> D & E;
C --> F;

This has two downsides though. The first is that because only live views are capable of handling system messages directly, if we want to use live data we end up having to move our data fetching and revalidation logic up to A regardless of if a lower common ancestor exists. For example:

flowchart TD;
subgraph "live view"
  A
  B
  C
  D
  E
  F
end
subgraph "data store"
  1[(1)]
  2[(2)]
end
1 & 2 -.-> A;
A --> B & C ;
B --> D & E;
C --> F;

This limitation is incidentally solved by LiveQuery, but the primary problem LiveQuery seeks to solve is the following. Imagine if we extended C to render another child, G:

flowchart TD;
subgraph "live view"
  A --> B;
  A --> C;
  B --> D;
  B --> E;
  C --> F;
  C --> G;
end

Now imagine G has a data dependency on 2:

flowchart TD;
subgraph "data store"
  1[(1)]
  2[(2)]
end
subgraph "live view"
  D
  E
  F
  G
end
D --> 2;
E --> 1;
E --> 2;
F --> 1;
G --> 2;

This would result in this new data loading scheme:

flowchart TD;
subgraph "data store"
  1[(1)]
  2[(2)]
end
subgraph "live view"
  A
  B
  C
  D
  E
  F
  G
end
A --> B;
A --> C;
B --> D;
B --> E;
C --> F;
C --> G;
1 -.-> A;
2 -.-> A;

Somehow an extension to C necessitated changes in C, B, and A!!! I could go on about how this violates the open-closed principle, but there's really no need. We all implicitly understand that small logical changes requiring big code changes is not good.

In an ideal world, an extension to C would result in a code change that was:

  1. scoped to C
  2. only additive

LiveQuery helps us get to this ideal world by decoupling data from rendering. A picture is worth a 1,000 words:

flowchart TD;
subgraph "data store"
  1[(1)]
  2[(2)]
end
subgraph "live view"
  A
  B
  C
  D
  E
  F
  G
end
subgraph "live query 1"
  LQ1{LiveQuery<1>}
end
subgraph "live query 2"
  LQ2{LiveQuery<2>}
end
1 -.-> LQ1;
2 -.-> LQ2;
LQ1 --> E;
LQ1 --> F;
LQ2 --> D;
LQ2 --> E;
LQ2 --> G;
A --> B;
A --> C;
B --> D;
B --> E;
C --> F;

By giving data and data loading a place to live outside of your component tree, your component tree becomes open to extension (at least wrt data dependencies). In other words you can add components with data dependencies to the tree without having to refactor data loading elsewhere in the tree. Here's what this looks like in code.

In your root live view module you'll add use LiveQuery. This will give you a live_query assign. You'll be required to pass this value through your entire component tree. This is how components access the data they want and get reredered when that data changes.

Now, in your components' you can call LiveQuery.Phoenix.LiveView.use_query/3 passing in your live_query as the first argument to access your data.

Note

The term access is intentional here. When using LiveQuery your data is not loaded by or stored in your live view/components. LiveQuery is responsible for data loading and storage and it provides an interface through which the data it maintains can be accessed. This means your should never need to store data from LiveQuery in non-temporary assigns.

That's almost it. The only missing piece is how you update this data in response to system messages (e.g. pubsub events). For example, if a component does

LiveQuery.Phoenix.LiveView.use_query(live_query, [:users, :list], fn -> Repo.all(User) end)

How do we ensure the query is re-run whenever new users are created/deleted/updated in the system?

The answer is simple. Queries are processes unto themselves. The above is roughly equivalent to:

defmodule UsersListQuery do
  @behaviour LiveQuery.Query

  @impl LiveQuery.Query
  def init(_state) do
    Repo.all(User)
  end
end

LiveQuery.Phoenix.LiveView.use_query(live_query, [:users, :list], UsersListQuery)

So, if we want to, for example, listen for pubsub events which might affect our query and then revalidate in response to those events we can do exactly that:

defmodule UsersListQuery do
  @behaviour LiveQuery.Query

  @impl LiveQuery.Query
  def init(_state) do
    Phoenix.PubSub.subscribe(:xyz, "users")
    Repo.all(User)
  end

  @impl LiveQuery.Query
  def handle_info({"users", _event, _payload}, _state) do
    Repo.all(User)
  end
end

LiveQuery.Phoenix.LiveView.use_query(live_query, [:users, :list], UsersListQuery)

Now, our query will rerun whenever our system emits user pubsub events, and LiveQuery will notify our live view when this happens by updating the live_query assign, causing our live view to rerender with our new data.

One more thing...

Because queries are processes which live under their own supervisor tree, and have their own life-cycles, they are fundamentally not tied to any one live component or live view.

In other words, a query can be used by anything.

  • Different instances of the same live component in the same live view instance? Sure!
  • Instances of different live components in the same live view instance? Sure!
  • Instances of different live components in different instances of the same live view? Sure!
  • Instances of different live components in instances of different live views? Sure!
  • Some random processes in your system that are entirely unrelated to live view? Sure!

This can dramatically reduce the memory consumption of your application. Instead of storing the same data in 1,000 different live view processes' assigns, you can store that same data in 1 LiveQuery query and access it everywhere!

Note

Currently query data is stored in the query process which means access requires sending a message with a copy of the data. However, I plan to swap this out with an ETS based approach that will improve this + make highly concurrent access better. This upgrade should be a non-breaking minor version upgrade.