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, code, and UI components. This page introduces the core concepts of reactivity in Genie applications and explain how they work together to create dynamic user interfaces.
Reactive variables and code
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.
Reactive code blocks are used to define the behavior of the application when a reactive variable changes in value. The blocks are defined using the @onchange
macro and they are triggered whenever a specified reactive variable's value changes, either from the frontend or the backend.
@app begin
@in N = 0
@out total = 0
@onchange N begin
print("N value changed to $N")
total = total + N
end
end
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.
@in
or @out
can only be modified within a block delimited by the @onchange
macro. Any changes made outside of it will not be reflected in the UI.Reactive UI components
Genie apps use in their UI Vue.js components from the Quasar framework, which provides a wide variety of responsive and reusable components for building feature-rich web applications. Reactivity is established by connecting the Quasar components to the reactive variables defined in the Julia backend. This is achieved through the v-model
attribute on the components, which binds the input elements to the reactive variables.
Below, using the low-code API we bind a 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 will be reflected in the N
reactive variable, and the reactive code block will be executed accordingly.
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 @route
:
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.