Skip to content

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:

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

kt
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.

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

kt
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.

kt
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.

kt
fun Html.Title(title: Signal<String>) {
    h1 {
        text(title)
    }
}

This function can now be called statically or reactively thanks to the compiler.

kt
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.

kt
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.

kt
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.

kt
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).

kt
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.

kt
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.

kt
val users = listOf(User(name = "Foo", age = 17))

for (user in users) {
    text("Name: ${user.name} Age: ${user.age}")
}