Skip to content

Why Vaadin Signals Still Feel Imperative (And How to Avoid It)

Problem

When I tried Vaadin signals, the code still looked imperative. I had to build components manually and then add effects on the side.

src/main/java/com/example/CounterView.java
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.signals.NumberSignal;
import com.vaadin.flow.signals.SignalFactory;
import com.vaadin.flow.signals.ComponentEffect;
@Route("counter")
public class CounterView extends VerticalLayout {
private final NumberSignal counter =
SignalFactory.IN_MEMORY_SHARED.number("counter");
public CounterView() {
Button button = new Button();
button.addClickListener(click -> counter.incrementBy(1));
add(button);
ComponentEffect.effect(button,
() -> button.setText(String.format("Clicked %.0f times", counter.value())));
}
}

The UI structure is one block, and the reactive behavior is another. It feels like two separate tracks.

Environment

  • Java 21
  • Kotlin 1.9.x
  • Vaadin 24.x (Flow)
  • Gradle 8.x

What happened?

I tried to render a list with signals too. I ended up removing all items and re-adding them on every change. That felt slow and hard to read.

src/main/java/com/example/ListView.java
import com.vaadin.flow.component.html.UnorderedList;
import com.vaadin.flow.component.html.ListItem;
import com.vaadin.flow.signals.ComponentEffect;
public class ListView {
public ListView(UnorderedList list) {
ComponentEffect.effect(list, () -> {
list.removeAll();
persons.value().forEach(personSignal -> {
ListItem li = new ListItem();
ComponentEffect.bind(li, personSignal, ListItem::setText);
list.add(li);
});
});
}
}

This works, but it hides the UI structure and forces me to think about re-rendering instead of state.

How to solve it?

I moved the structure into a Kotlin DSL and bound state directly to properties. That keeps the UI tree readable.

First I added a tiny reactive type and binding helpers.

src/main/kotlin/com/example/Signal.kt
package com.example
class Signal<T>(initial: T) {
private var valueInternal: T = initial
private val listeners = mutableListOf<(T) -> Unit>()
val value: T
get() = valueInternal
fun subscribe(listener: (T) -> Unit) {
listeners.add(listener)
listener(valueInternal)
}
fun update(newValue: T) {
valueInternal = newValue
listeners.forEach { it(newValue) }
}
}
src/main/kotlin/com/example/Bindings.kt
package com.example
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.HasText
fun HasText.text(signal: Signal<String>) {
signal.subscribe { this.text = it }
}
fun Component.visible(signal: Signal<Boolean>) {
signal.subscribe { this.isVisible = it }
}

Then I built the UI in one place. The bindings sit next to the component they affect.

src/main/kotlin/com/example/CounterView.kt
package com.example
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.html.Span
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.router.Route
@Route("counter")
class CounterView : VerticalLayout() {
init {
val count = Signal(0)
val label = Span()
label.text(count.map { "Clicked $it times" })
val button = Button("Add")
button.addClickListener { count.update(count.value + 1) }
add(label, button)
}
}

I used a minimal list example too. It is still simple, but the UI structure is readable.

src/main/kotlin/com/example/ListView.kt
package com.example
import com.vaadin.flow.component.html.ListItem
import com.vaadin.flow.component.html.UnorderedList
import com.vaadin.flow.component.orderedlayout.VerticalLayout
import com.vaadin.flow.router.Route
@Route("list")
class ListView : VerticalLayout() {
init {
val items = Signal(listOf("A", "B", "C"))
val list = UnorderedList()
items.subscribe { values ->
list.removeAll()
values.forEach { value ->
list.add(ListItem(value))
}
}
add(list)
}
}

This is not a perfect diffing solution, but the layout and the state changes are in one spot. That is already easier for beginners to reason about.

The reason

I think the key reason the official signals feel imperative is the split between layout and effects. When I keep bindings next to the component, the code reads like a UI tree again. It feels like writing a recipe instead of a list of edits.

Summary

In this post, I explained why Vaadin signals still feel imperative and how I avoid that style. The key point is keeping UI structure in a DSL and binding state directly to component properties.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments