Skip to content

Commit

Permalink
reactivity page rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
PGimenez committed Oct 23, 2024
1 parent 6679ca5 commit b798437
Showing 1 changed file with 195 additions and 62 deletions.
257 changes: 195 additions & 62 deletions content/2.framework/3.stipple.jl/0.docs/1.reactivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,21 @@ toc: true
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 code

**Reactive variables**
The reactive code in a Genie app is implemented in the block delimited by the `@app` macro, which holds the definitions for the reactive variables and handlers:

```julia
using GenieFramework
@genietools
# Reactive code
@app begin
# Reactive variables and handlers are defined here
end
```
This code block can only contain reactive variable and handler definitions, which are explained over the next two sections.

## 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:

Expand All @@ -20,22 +32,37 @@ Reactive variables are used to store the state of UI components, allowing the ba

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:
`@in` and `@out` 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:

```julia
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.
Whenever the user types in the text field in the browser, the `msg` variable will be updated in the backend.

**Rective handlers**
Reactive variables must be initialized to a constant value of the appropriate type, which may come from a previously defined variable or assigned at declaration time.
```julia
const total_N = 100
@app begin
@in N = total_N
@in M = 25
end
```

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.

Moreover, you cannot use a reactive variable to initialize another variable, as in the following example:
```julia
@in N = 0
@in M = N + 1
```
This code will throw an error due to `N` not existing.


## Reactive handlers

Reactive handlers define the code that is executed when a reactive variable changes in value. The first two types of 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.

```julia
@app begin
Expand All @@ -45,10 +72,40 @@ Reactive handlers define the code that is executed when a reactive variable chan
msg_length = length(msg)
end
end
ui = [textfield("Message", :msg), p("Length: {{msg_length}}")]
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.

Handlers can also be triggered by modifying their watched variable from another handler:

```julia
@app begin
@in N = 0
@in M = 0
@onchange N begin
M = N+1
end
@onchange M begin
@info "M changed to $M"
end
end
```
Moreover, it is possible to modify a reactive variable without triggering its handler by appending the \[!\] suffix to it:

```julia
@app begin
@in N = 0
@in M = 0
@onchange N begin
M[!] = 0 # this won't trigger the handler
M = N+1
end
@onchange M begin
@info "M changed to $M"
end
end
```

The `@onbutton` macro is used to watch booleans, and it sets their value to `false` after the handler is executed.

```julia
@in trigger = false
Expand All @@ -58,47 +115,115 @@ The `@onbutton` macro is used to watch booleans, and it sets their value to `fal
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:
Finally, some components like the [file uploader](/geniebuilder/docs/ui-components#file-uploader) emit events when the user interacts with them. These events can be intercepted in the backend with the `@event` macro as in this example:

```julia
textfield("N", :N )
@event uploaded begin
@info "File uploaded"
@notify("File uploaded")
end
@event rejected begin
@info "rejected"
@notify("File rejected")
end
```

The resulting HTML code includes the `v-model` attribute, which connects the input field to the N reactive variable:
```html
<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.
## Reactive variable scoping

### 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:
```julia
@in N = 0
@in M = N + 1
```
This code will throw an error due to `N` not existing.
There are some concepts to keep in mind when writing reactive code:

1. Reactive variables can only be modified within a handler implemented with the `@onchange`, `@onbutton` or `@event` blocks.

<br>

Any change attempts made to a reactive variable outside a handler will not modify the reactive variable. For example, in the example below the assignment `N = 45` won't modify the declared reactive `N`, but will create a new `N` variable in the module scope. Only the assignment `N = N + X` inside the handler will make changes to the reactive variable.

```julia
@app begin
@in N = 0
@in X = 0
N = 45
@onchange X begin
N = N + X
end
end
```

2. The reactive variables watched by a handler are passed to the handler by value, not by reference.

<br>

This is relevant when running a long task inside a handler that depends on the attached reactive variable, like in this snippet:

```julia
@app begin
@in toggle_on = false
@onchange toggle_on begin
while toggle_on == true begin
@info "Toggle set to true"
sleep(1)
end
end
end
ui() = toggle("Toggle", :toggle_on)
```
Once the toggle is clicked and `toggle_on` set to `true`, the handler will execute and enter the while loop. In this loop, `toggle_on` will **always** be true since it was was passed by value to the handler. So, if we set the toggle to `false` in the UI the reactive variable will be updated but the loop will not exit.

To check the global value of `toggle_on`, we need to directly access the reactive variable as stored in the reactive model `__model__`

```julia
@onchange toggle_on begin
# This loop will check the referenced value instead of what's passed to the handler
while __model__.toggle_on[] == true begin
@info "Toggle set to true"
sleep(1)
end
end
```
See [here](/framework/stipple.jl/docs/reactivity#under-the-hood-reactive-models) to learn how the reactive model works.

3. Variables declared within an `@onchange` block are scoped, meaning that they will not overwrite global variables but create new ones.

<br>

To modify a global variable, you must use the `global` keyword:
```julia
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 inside the handler
end
end
```

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:
## Composite objects and reactivity

Modifying a field in a composite object like an array or a struct in the Julia code won't trigger a value synchronization to the UI. To trigger the synchronization, the object needs to be reassigned or synced manually with the `@push` macro as this example shows:

```julia
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
@out x = collect(1:10)
@out y = randn(10)
@in add_data = false
# How to update array data
@onbutton add_data begin
push!(x, length(x)+1) # This will not trigger an update in the UI
@push x # This will send the value to the UI
y = vcat(y, randn(1)) # Variable reassignments also trigger UI updates
end
end
```

Similarly, a handler won't be triggered when a field in an object is modified, only when the entire object is assigned a new value.


### Variable types
## Using custom types as reactive variables

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:

Expand All @@ -123,44 +248,52 @@ ui() = [p("{{d.description}}"),p("{{d.data}}"),p("{{d.data.c}}")]
up()
```

If some object cannot be serialized, you'll need to specialize `Stipple.render` to make it work.

### Recursive reactivity
If some object cannot be serialized, you'll need to specialize `Stipple.render` and `Stipple.stipple_parse` to make it work. For example, this specialization allows sending a `Matrix` type to the browser as an array of arrays:

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.
```julia
Stipple.render(X::Matrix) = [X[:, i] for i in 1:size(X, 2)]
Stipple.stipple_parse(T::Type{Matrix}, X::Matrix) = hcat(X...)
```

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.
## Under the hood: reactive models and observables

<img style="display:block;width:90%;max-width:100%;margin-left:auto;margin-right:auto" src="/assets/docs/reactiveui/recursive.gif">
Reactive variables are stored as fields in [a struct named `__model__`](http://localhost:3000/framework/stipple.jl/docs/reactivity#under-the-hood-reactive-models), which is only exposed inside the `@app` block. The content of a model can be printed from a handler like this:

```julia
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
@in N = 0
@onchange isready begin
@show __model__
@show __model__.N
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()
```julia
__model__ = Instance of 'Main_ReactiveModel'
channel__ (internal): QTQJPSJHFPNOEKMXSAUSRBPZPEWYPWIP
modes__ (internal): LittleDict{Symbol, Int64, Vector{Symbol}, Vector{Int64}}()
isready (autofield, in): true
isprocessing (autofield, in): false
fileuploads (autofield, in): Dict{AbstractString, AbstractString}()
N (in): 0
__model__.N = Reactive{Int64}(Observable(0), 1, false, false, "/Users/pere/genie/mwes/reactivemodel/app.jl:5")
```
Notice that the variable `N` is an `Observable` from the [Observables.jl](https://github.com/JuliaGizmos/Observables.jl) package, which implements the reactivity Stipple relies on. Thus, one can directly interact with the observable via the `__model__` struct and access its value as `__model__.N[]`.

Moreover, Stipple translates its handlers to `ObserverFunctions` from `Observables.jl`, which are defined like this:

```julia
obs_func = on(observable) do val
println("Got an update: ", val)
end
```
Notice that the handler is an anonymous function taking the observable value, which explains the scoping limitations discussed in point 2 of the [Reactive variable scoping](/framework/stipple.jl/docs/reactivity#reactive-variable-scoping) section.

### Under the hood: reactive models
## Under the hood: reactive storage and handlers

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:
When reactive variables and handlers are defined, they are stored in the `REACTIVE_STORAGE` and `HANDLERS` dictionaries of the `GenieFramework.Stipple.ReactiveTools` module. For example, these storage objects may look like this:

```julia
julia> GenieFramework.Stipple.ReactiveTools.REACTIVE_STORAGE[Main]
Expand Down

0 comments on commit b798437

Please sign in to comment.