Reactivity
Reactivity is a concept that allows UI to react to changes in our application's state.
Explaining Reactivity
Reactive values are values of Signal<T>
. A Signal<T>
simply represents a value that may change over time.
You can create one using the signal(T)
function:
val count: MutableSignal<Int> = signal(initialValue = 1)
count.value++
count.subscribe { println(it) } // "1"
count.value++ // "2"
Subscribing a Signal<T>
will call the block, first with the initial value, and again for each subsequent update.
To create a Signal<T>
that is never updated, you can use the just(T)
function:
val name = just("Foo")
Derived Signal
It is common to have reactive values that are "derived" from other values. This means that when the original value changes, the derived value also updates.
You can simply attach a trailing lambda to any signal to transform it into a derived signal.
val count = signal(1)
val doubleCount = count { it * 2 }
assert(doubleCount.value == 2)
count.value = 4
assert(doubleCount.value == 8)
Compiler
The Komponent Compiler allows you to call any function that accepts a Signal<T>
with just a regular value:
fun log(signal: Signal<String>) {
signal.subscribe { println(it) }
}
log("Hello, world!") // "Hello, world!"
Reactive Components
Komponent tags accept reactive values for all attributes. This means that you can always use a static (in this context, static means any regular old value) or reactive a value.
fun Html.Counter() {
val count = signal(1)
val value = count { "Value: $count" }
button(onClick = { count.value++ }) {
text(value)
}
}
You can also accept a Signal
value to allow any value to be optionally reactive.
fun Html.Title(title: Signal<String>) {
h1 {
text(title)
}
}
This function can now be called statically or reactively thanks to the compiler.
fun Html.MyBlog() {
Title("My Blog")
val title = signal("My Blog")
Title(title)
}
Reactive Flow Components
A single Signal
only represents a single value. On their own, they don't support other kinds of values such as lists, other components or anything that requires conditional rendering.
When
The When
component is used to show a component when a condition holds. The When
is best used for conditions that may change over time.
fun Html.Details() {
val expanded = signal(false)
button(onClick = { expanded.value = !expanded.value }) {
text("Click to expand")
}
When(expanded) {
text("Hidden content")
}
}
When
also supports fallback rendering to handle the else
case.
fun Html.Details() {
When(expanded, fallback = { text("Content is hidden") }) {
text("Hidden content")
}
}
The When
component only exists because the logic to replace the internal logic to reactively remove and re-add the children, and it is necessary for unchanging values as an ordinary if
statement will still work.
Dynamic
Sometimes, you want to rerender an entire component when a value changes, for this you can use the Dynamic
component. It takes a Signal
and renders the children every time the signal is updated.
fun Html.Tabs(tab: MutableSignal<Tab>) {
button(onClick = { tab.value = Tab.FIRST }) { text("first") }
button(onClick = { tab.value = Tab.SECOND }) { text("Second") }
Dynamic(tab) {
when (it) {
FIRST -> {}
SECOND -> {}
}
}
}
Lists
Reactive lists use a different kind of signal known as a ListSignal
since signals cannot track mutations to the value, only mutations of the value itself.
To create a list signal, use flowList(List<E>)
or flowListOf(*E)
.
data class User(val name: String, val age: Int)
val users = flowListOf(User(name = "Foo", age = 17))
button(onClick = { users.add(User(name = "Bar", age = 17)) }) {
text("Add new user")
}
A ListSignal
can be modified like any other list. However, it can also be used as a prop to the For
component which renders a dynamic list of elements.
For(each = list) { user ->
text("Name: ${user.name} Age: ${user.age}")
}
Similarly to with the When
component, it may not be necessary for non-reactive values as a plain old for
will suffice.
val users = listOf(User(name = "Foo", age = 17))
for (user in users) {
text("Name: ${user.name} Age: ${user.age}")
}