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.
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.
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.
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) } }}package com.example
import com.vaadin.flow.component.Componentimport 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.
package com.example
import com.vaadin.flow.component.button.Buttonimport com.vaadin.flow.component.html.Spanimport com.vaadin.flow.component.orderedlayout.VerticalLayoutimport 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.
package com.example
import com.vaadin.flow.component.html.ListItemimport com.vaadin.flow.component.html.UnorderedListimport com.vaadin.flow.component.orderedlayout.VerticalLayoutimport 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:
- π¨βπ» Vaadin Signals (Preview)
- π¨βπ» Vaadin Components
- π¨βπ» Kotlin Type-Safe Builders
- π¨βπ» Vaadin Flow Overview
Oh, and if you found these resources useful, donβt forget to support me by starring the repo on GitHub!
Comments