Reactivity
Reactivity in Genie apps allows developers to create interactive and responsive user interfaces that automatically update when the underlying data changes or the user interacts with the UI. This is accomplished using a combination of reactive variables, handlers, and UI components. This page introduces the core concepts of reactivity in Genie applications and explains how they work together to create dynamic user interfaces.
Reactive variables, handlers and UI components
Reactive variables
Reactive variables are used to store the state of UI components, allowing the backend to be aware of the changes made in the frontend and vice-versa. Reactive variables are defined using the @in
and @out
macros inside the @app
block, and each indicates the following:
@in
: this variable can be modified from the UI and its changes will be propagated to the backend.@out
: this variable is read-only and cannot be modified from the UI. However, it can be updated from the backend to reflect changes in the data.
Additionally, there's the @private
macro to define reactive variables that are not exposed to the UI. These variables will be copied to every user session, and any changes made to them will not be propagated to the UI or other users. Still, these variables can trigger a reactive handler.
Reactive variables are bound to UI components to store their state information, like the number selected with a slider, or the content of a text field, and trigger a handler whenever the variable's content changes. For instance, we can bind the textfield
component to a variable as:
using GenieFramework
@app begin
@in msg = ""
end
ui() = textfield("Message", :msg )
@page("/", ui)
Whenever the user enters text in the field in the browser, the msg
variable will be updated in the backend.
Rective handlers
Reactive handlers define the code that is executed when a reactive variable changes in value. The handlers are defined using the @onchange
or @onbutton
macros, and they are triggered whenever a specified reactive variable's value changes, either from the frontend or the backend.
@app begin
@in msg = ""
@out msg_length = 0
@onchange N begin
msg_length = length(msg)
end
end
ui = [textfield("Message", :msg), p("Length: {{msg_length}}")]
The @onbutton
macro is used to watch booleans, and it sets their value to false
after the handler is executed.
@in trigger = false
@onchange true begin
print("Action triggered")
end
ui = [btn("Trigger action", @click(:trigger))]
Reactive UI components
Reactive variables are bound to UI components to store their state information, like the number selected with a slider, or the content of a text field, and trigger a handler whenever the variable's content changes. For instance, we can textfield
component to the reactive variable N
from the previous example:
textfield("N", :N )
The resulting HTML code includes the v-model
attribute, which connects the input field to the N reactive variable:
<q-input label="N" v-model="N"></q-input>
This ensures that any change in the value of the input field in the browser will be reflected on N
in the Julia code, and the reactive code block will be executed accordingly. Likewise, any change to N
in the backend will update the input field in the browser.
Defining reactive variables
The definition of a new reactive variable requires an initial value of the appropriate type. For instance, in the example above, both N
and total
are of type Int
. If the value introduced in the UI for N
is a Float
, the app will throw an error. Moreover, you cannot use a reactive variable to initialize another variable, as in the following example:
@in N = 0
@in M = N + 1
This code will throw an error due to N
not existing.
Reactive variables can only be modified within a handler implemented with the @onchange
or @onbutton
macros. Any changes made outside of it will not be reflected in the UI. This is because these variables reside within the @app
block, and they are instantiated for each user session.
Finally, variables declared within an @onchange
block are scoped, meaning that they will not overwrite global variables but create new ones. To modify a global variable, you must use the global
keyword:
N = 0
M = 0
@app begin
@in toggle = false
@onchange toggle begin
global N = N + 1
M = M+1 # This will create a new variable M
end
end
Reactive UI components
Variable types
Reactive variables are serialized and sent to the browser as Javascript objects. Most base Julia types, like, for example, Int
, String
, Vector{String}
, Dict
, can be declared as reactive. Moreover, custom struct definitions can also be exposed to the UI like in this example:
using GenieFramework
@genietools
mutable struct MyContent
c::Int
end
mutable struct MyData
description::String
data::MyContent
end
@app begin
@out d = MyData("hello", MyContent(1))
end
ui() = [p("{{d.description}}"),p("{{d.data}}"),p("{{d.data.c}}")]
@page("/", ui)
up()
If some object cannot be serialized, you'll need to specialize Stipple.render
to make it work.
Recursive reactivity
In general, composite objects are not recursively reactive. This means that changing one of their fields will not always trigger a reactive handler. With dictionaries, for instance, changing a dict field in the Julia code will not propagate the new value to the browser. Only replacing the entire dict, or changing a field in the browser, will trigger a handler and propagate changes.
The example below depicts this behavior. There are three buttons to modify the data
field in a dict: one runs code in the browser, and the other two trigger a handler in the backend. The reactive handler watching the dictionary d
is only triggered when pressing the first (frontend) and third (dict replacement) buttons. Modifying the field from the backend using the second button increases the counter but does not trigger the handler.

using GenieFramework
@genietools
@app begin
@in d = Dict("description" => "hello", "data" => 1)
@in change_field = false
@in replace_dict = false
@onchange d begin
@show d
end
@onbutton change_field begin
d["data"] += 1
end
@onbutton replace_dict begin
d = Dict("description" => d["description"], "data" => d["data"] + 1)
end
end
ui() = [p("{{d.data}}"), btn("Frontend +1 to field", @click("d.data += 1")), br(), btn("backend +1 to field", @click(:change_field)), br(), btn("backend replace dict", @click(:replace_dict))]
@page("/", ui)
up()
Under the hood: reactive models
Reactive models work by maintaining an internal representation of reactive variables and code blocks. When you define reactive variables and code blocks, they are stored in the REACTIVE_STORAGE
and HANDLERS
dictionaries of the GenieFramework.Stipple.ReactiveTools
module. For example, the storage for the @app
block in the previous example contains:
julia> GenieFramework.Stipple.ReactiveTools.REACTIVE_STORAGE[Main]
LittleDict{Symbol, Expr, Vector{Symbol}, Vector{Expr}} with 6 entries:
:channel__ => :(channel__::String = Stipple.channelfactory())
:modes__ => :(modes__::Stipple.LittleDict{Symbol, Any} = $(QuoteNode(LittleDict{Symbol, Any, Vector{Symbol}, Vector{Any}}())))
:isready => :(isready::Stipple.R{Bool} = false)
:isprocessing => :(isprocessing::Stipple.R{Bool} = false)
:N => :(N::R{Int64} = R(0, 1, false, false, "REPL[2]:2"))
:total => :(total::R{Int64} = R(0, 4, false, false, "REPL[2]:3"))
julia> GenieFramework.Stipple.ReactiveTools.HANDLERS[Main]
1-element Vector{Expr}:
quote
#= /Users/pere/.julia/packages/Stipple/pgem3/src/ReactiveTools.jl:689 =#
on(__model__.N) do N
#= /Users/pere/.julia/packages/Stipple/pgem3/src/ReactiveTools.jl:690 =#
#= REPL[2]:5 =#
print("N value changed to $(N)")
#= REPL[2]:6 =#
__model__.total[] = __model__.total[] + N
end
end
When a user makes an HTTP request to a route, a new ReactiveModel
instance is created from the storage for that specific user session. This ensures that each user has an isolated state and can interact with the application independently, without affecting the state of other users.
The model instantiated for the request can be accessed with @init
when using the route
function instead of @page
:
route("/") do
model = @init
@show model
page(model, ui()) |> html
end
var"##Main_ReactiveModel!#292"("OSINKNHRJHNKBFXCFZVKSWQVMWTUMUNN", LittleDict{Symbol, Any, Vector{Symbol},
Vector{Any}}(), Reactive{Bool}(Observable(false), 1, false, false, ""), Reactive{Bool}(Observable(false), 1, false, false, ""),
Reactive{Int64}(Observable(0), 1, false, false, "REPL[2]:2"), Reactive{Int64}(Observable(0), 4, false, false, "REPL[2]:3"))
When the new reactive model instance is created, it is assigned a unique identifier, which is used to track the user's session and maintain the state for the entire duration of the session. This identifier is used by the Genie server to route the websocket messages to the appropriate reactive model instance. Communication between the frontend and the backend is facilitated by websockets, which provide real-time, bidirectional communication channels between the client and the server. When a reactive variable's value changes in the frontend or backend, a websocket message is sent to the other side containing the updated value.
Introduction
The Stipple.jl package in Genie Framework implements a low-code API that helps Julia developers quickly and easily create interactive data dashboards and data-centric applications, as well as web-based user interfaces for Julia software. Moreover, it offers a large collection of user interface elements (such as inputs, buttons, sliders, tabs, data tables, responsive layouts, and many more) and powerful plotting capabilities.
Low-code UI API
Implement UIs in pure Julia with the low-code API.