How to evaluate a LiveBook cell with a value **asynchronously** set in the previous cell

Hi All

Context
I am creating a small DB analysis tool in LiveBook to explore what we have in our job matching service. Conceptually this is just several relatively simple cells:

  1. Select a job opening
  2. Select an applicant
  3. Display something about applicant

Choose in the previous cell, update in the next cell helps
People at Select something in one LiveBook cell, update list in another cell - #3 by hugobarauna helped me to figure how to use Kino.listen so that “Select an applicant” set was able to react to choices done in the “Select a job opening” step and show the applicant list only for the selected opening.

It works very well except for the first render of the “Select an applicant step”

The problem
By the moment of the very first render of “Select an applicant step”, job opening selector could be clicked multiple times already and I wail to figure how to pass the current value further (except via ssome external storage such :ets , but that sounds like an ugly overkill).

If that was a LiveView, I would just update process assigns from async callback.
What would be the way to make it work in LiveBook? Is there some sort of “current book context” that could be accessed and updated somehow?

Here’s code of what I want to achieve in a very simplified way:

First cell:

# First cell

button = Kino.Control.button("Click")
counter = 0

Kino.listen(button, fn _event ->
  IO.puts("button clicked, counter is #{counter}")

  # Obviously it won't work as it just createa a local variable,
  # it can't update the earlier defined one
  counter = counter + 1
end)

button

Second cell:

# Second cell should be able to set current counter value somehow
IO.puts("Counter as set by the moment of this cell evaluation:#{counter}")

Kino.listen(button, fn _event ->
  IO.puts("Do something with the current counter of #{counter}, maybe increase it as well")
end)

Hey @artem!

Is the issue that reacting to “select a job opening” takes time (querying data), so the user can change the value multiple times, while the data is still loading?

One way to solve this directly would be to use Kino.listen/3 with state. You would do the query in a Task.async, store the task in the state, so if another event arrives, you can kill the existing task (if applicable) and start a new one.

Another way would be to manage the whole form as a single frame and have a GenServer rendering into it. With that you could render “Loading…” or similar after changing the value.

There is a PR in Kino that you can give a try: Add Kino.Screen by josevalim · Pull Request #489 · livebook-dev/kino · GitHub ({:kino, github: "livebook-dev/kino", branch: "jv-kino-screen"}). It adds an abstraction for building more complex event-driven forms/wizards.

Querying is fast (well, fast enough for my research trials), I was just annoyed that I cannot pass value set on button click from cell A, to cell B except if I just command from cell A’s Kino.listen to re-render cell B’s frame. And for cell B frame to be available from cell A I’ll need to create modules, reorder stuff carefully… lots of action for something that IMHO should be simple exactly in notebooks created for analyzing data.

By now I have half-solved it by using Agent as a centralized data storage from notebook.

Now cell B can also subscribe to cell A’s clicks, but actually get it’s data from the Aagent.

It still causes a bit of mess, because cell B code needs to be somehow activated when it needs to refetch Agent, but somewhat works.

Or is there possibly a way to somehow subscribe (or listen) in Livebook for the Agent state changes? Such reactivity would make my code way simpler.

There are two paths you can take to write an app:

  1. Separate cells, relying on Kino.Input.read and automatic cell reevaluation. Whenever the input value changes, the dependent cell reevaluates. The benefit in your case is that while a cell is evaluating, it doesn’t matter how many times you change the select value, the next time the cell reevaluates it will just get the latest one.

  2. Using forms and events. When the form is dynamic, you basically want a GenServer with state that manages form events and renders UI into the frame. The idea with the new Kino.Screen abstraction is to make this workflow easier. Since mentioned Agent and shared state, using a single process is a more natural abstraction in this case. This also implies writing pretty much everything in a single cell, which I agree diverges from how you would write a typical notebook, but it is somewhat inherent for a stateful, event-driven app.

From a more technical perspective, the core principle in Livebook notebook evaluation is reproducability. Reading input values and reevaluating cells fits into this model (where inputs are conceptually similar to variables assigned via UI). On the other hand, when we think about events such as button clicks and form submissions, we shift to a different paradigm.

Both 1. and 2. have their use cases. One of the key differences is that with 1. the inputs are shared across everybody entering the page, while with 2. we can build a single app that handles multiple users, and provides each of them with separate interactions, but can also make the experience shared (e.g. a chat).

Ideally we would he a single way of doing things, but so far the consensus has been that each of them has a place.