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.

Reactive variables tagged with @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.