Plots Reactivity
Learn how reactivity works in Plot notebooks
Inside a Plots notebook, cells will automatically rerun in response to a change in widget values. This feature is powered by a generic reactivity system that can be used to encode aribtrary data dependencies between parts of a notebook’s code.
Introduction
The reactive system is based on two primitive objects:
- signals, which store a reactive value,
- and computation nodes, which store a reactive function.
Computation nodes that read a signal value will be automatically subscribed to the signal. Later, when a signal value changes, all its subscribers get re-executed.
This is how the familiar example of widget reactivity works:
- Widgets are created alongside a signal for their value.
- Cells are implicitly wrapped up in computation nodes.
- When a cell runs, it may access a widget value through the signal. If it does, the signal records that the cell’s computation node needs to rerun when the value changes.
- User input causes the UI to send an update to the kernel, which updates the widget value signal, which reruns all of its subscribers.
Custom Signals
Signals can be created at any time, independent of any widgets.
To read the signal’s value, call it with no arguments. This will subscribe the current computation node to the signal:
To update the signal’s value, call it with the new value as an argument:
The signal stores a reference to the value and will not react to changes within the stored object. It is best practice to treat signal values as immutable:
Avoid infinite update loops by making sure a cell never unconditionally updates a signal that it is subscribed to:
.sample()
allows reading a signal value without subscribing to its changes:
Reactive Transactions
Signal updates are delayed until the current cell finishes running all the way through:
This somewhat unintuitive behavior is a best practice established in reactive systems design as it helps keep the overal state of the system consistent at all times.
In the previous example value
and value_str
are obviously related. The delayed updates ensure that the relationship is preserved at all times—cell 2 will never trigger an AssertionError
.
Contrast this with a hypothetical system that applies updates immediately.
A reactive transaction can actually include more than one cell. If a signal update causes multiple cells to rerun, all of these cells will belong to a transaction:
Note that we do not see any of the intermediate states e.g. cell_print 30 2
, cell_print 11 40
; which could be printed in an immediate-update system.
Reactivivity with Conditionals
Each time a cell reruns, it starts from a fresh state and will forget all its previous subscriptions. This means that a cell is only subscribed to the signals it access during its last run.
Cells with conditionals can change their subscriptions on each run:
If the value of value
changes to 50
, the cell will execute the conditional and thus access and subscirbe to value_str
.
If necessary, the cell can always uncoditionally subscribe to a signal by just accessing its value and not using it:
Redefining Signals
In Python, setting a variable will dispose of the old value entirely:
This is annoying with signals, as the cell defining the signal can rerun and then the signal would be recreated with no subscribers. There is a special rule in Plots notebooks that global variables holding a signal will not be disposed of when overriding them. Instead, the signal value will be updated:
To replicate the default behavior and throw out the old signal, use del
:
When using local variables, no special rule applies. To avoid overriding a signal in a local variable, check locals()
:
Cheatsheet
x = Signal(initial)
defines a new signal with value set toinitial
x()
reads the value of the signal and subscribesx(10)
sets the value of the signalx.sample()
reads the value of the signal without subscribing. Useful to avoid infinite update loops- Signal updates are delayed until the current transactions ends
- Each time, all updated cells execute in one transaction
- Cells only subscribe to signals they accessed during the last run
- Signal values should be treated as immutable
- Re-assigning a signal to a global variable will override the value but not clear subscribers
Was this page helpful?