What is Declarative UI Pattern in Server-Side Development?
Purpose
This post shows how declarative UI patterns work on the server side and why backend developers should care. I’ll explain what “declarative UI” means, how it differs from traditional imperative approaches, and why a Reddit developer implemented React-like signals in Kotlin for Vaadin.
Environment
- Kotlin 1.9+
- Vaadin 24+
- Java 17+
- Server-side rendering pattern
The Problem: Imperative UI Hell
I recently read a Reddit post from a full-stack developer who loved React’s declarative approach but was stuck working with Vaadin’s imperative API. Their problem hit home for me.
Here’s what they were dealing with - traditional imperative UI code:
class UserView : VerticalLayout() { private val nameLabel = Label("Guest") private val emailLabel = Label("") private val editButton = Button("Edit")
fun setUser(user: User) { nameLabel.text = user.name // Manual update 1 emailLabel.text = user.email // Manual update 2 editButton.isEnabled = user.canEdit // Manual update 3 // What if we add a new field? Must remember to update it here! }
fun onUserUpdate(newUser: User) { setUser(newUser) // Must call this manually }}I think this is painful because:
- Easy to forget updates: When you add a new UI element, you must remember to update it in
setUser() - State synchronization bugs: When multiple parts of code modify the same data
- Hard to track updates: You can’t easily see what triggers UI changes
The Reddit author called this “stack multiplication” - you need to learn:
- Frontend framework (React/Vue)
- API layer design (REST/GraphQL)
- State management (Redux, Zustand)
And then there’s “excessive decoupling” - business logic gets duplicated between backend validation and frontend validation.
What is Declarative UI?
Declarative UI means you describe what your UI should look like based on state, and the framework automatically updates the view when that state changes.
Instead of:
// Imperative: HOW to updatenameLabel.text = user.nameeditButton.isEnabled = user.canEditYou write:
// Declarative: WHAT to showval nameLabel = Label(computed { user.value?.name })val editButton = Button("Edit").apply { isEnabled = computed { user.value?.canEdit == true }}The framework handles the “how” - it figures out which parts of the UI need updating when user.value changes.
The Solution: Reactive Signals in Kotlin
The Reddit developer implemented React-style reactive signals in Kotlin for Vaadin. Let me show you what this looks like:
class Signal<T>(initialValue: T) { private var value = initialValue private val dependencies = mutableSetOf<Effect>()
fun get(): T { // Track which effects are reading this signal currentEffect?.let { dependencies.add(it) } return value }
fun set(newValue: T) { value = newValue // Notify all dependent effects to re-run dependencies.forEach { it.run() } }}
// Convenience property syntaxvar Signal<T>.value: T get() = get() set(value) = set(value)Then they added computed() for derived state:
fun <T> computed(computation: () -> T): Signal<T> { val result = Signal(computation()) val effect = Effect { result.value = computation() } effect.run() return result}And effect() for side effects (like updating UI):
class Effect(private val fn: () -> Unit) { fun run() { currentEffect = this try { fn() } finally { currentEffect = null } }}
private val currentEffect = ThreadLocal<Effect?>()How It Works in Practice
Let me rewrite the imperative example using these signals:
class UserView { // State as signals private val user = Signal<User?>(null) private val displayName = computed { user.value?.name ?: "Guest" } private val canEdit = computed { user.value?.canEdit == true }
// UI components depend on computed signals private val nameLabel = Label(displayName) // Auto-updates private val emailLabel = Label(computed { user.value?.email ?: "" }) private val editButton = Button("Edit").apply { isEnabled = canEdit // Auto-updates }
// Updating state is all that's needed fun onUserUpdate(newUser: User) { user.value = newUser // UI updates automatically! }}When I call onUserUpdate(newUser):
user.valuechanges- All computed signals that depend on
userrecalculate - Effects that depend on those computed values re-run
- UI components automatically update
I don’t need to remember to call refresh() or updateUI() - it just works.
Server-Side vs Client-Side
I want to clarify something important. Declarative server-side UI is different from React:
React (client-side):
- Runs in the browser (JavaScript)
- Browser does the rendering
- Fast local updates
Declarative server-side (Vaadin, Blazor, LiveView):
- Runs on the server (Kotlin, Java, C#)
- Server sends UI deltas over WebSocket
- No JavaScript required from you
The Reddit author’s implementation keeps everything in the JVM:
- Backend is single source of truth
- No separate frontend codebase
- Business logic lives in one place
Real Example: Todo App
Let me show you a complete example. Here’s a simple todo app using this pattern:
class TodoApp { // State private val todos = Signal<List<Todo>>(emptyList()) private val newTodoText = Signal("") private val filter = Signal<Filter>(Filter.All)
// Derived state private val filteredTodos = computed { when (filter.value) { Filter.All -> todos.value Filter.Active -> todos.value.filter { !it.done } Filter.Completed -> todos.value.filter { it.done } } }
private val itemsLeft = computed { todos.value.count { !it.done } }
private val canAdd = computed { newTodoText.value.isNotBlank() }
// UI val ui = VerticalLayout( // Input section HorizontalLayout( TextField().apply { placeholder = "What needs to be done?" value = newTodoText }, Button("Add").apply { isEnabled = canAdd onClick { addTodo() } } ),
// Filter buttons HorizontalLayout( Button("All").apply { onClick { filter.value = Filter.All } }, Button("Active").apply { onClick { filter.value = Filter.Active } }, Button("Completed").apply { onClick { filter.value = Filter.Completed } } ),
// Todo list ListView(filteredTodos) { todo -> HorizontalLayout( Checkbox().apply { value = todo.done onClick { toggleTodo(todo.id) } }, Label(todo.text) ) },
// Footer Label(computed { "${itemsLeft.value} items left" }) )
// Actions private fun addTodo() { todos.value = todos.value + Todo( id = UUID.randomUUID(), text = newTodoText.value, done = false ) newTodoText.value = "" // Clear input }
private fun toggleTodo(id: UUID) { todos.value = todos.value.map { todo -> if (todo.id == id) todo.copy(done = !todo.done) else todo } }}
enum class Filter { All, Active, Completed }data class Todo(val id: UUID, val text: String, val done: Boolean)I want to point out a few things:
- No manual
refresh()calls anywhere - UI components subscribe to signals automatically
- Adding a new todo updates all derived state automatically
Common Mistakes
From the Reddit discussion, I found these common mistakes:
1. Mixing imperative and declarative:
// Bad: Imperative listener with declarative signalsbutton.onClick { user.value = newUser nameLabel.refresh() // Don't need this!}2. Over-computing:
// Bad: Computed for static valuesval appName = computed { "My App" } // Never changes
// Good: Just use a constantval appName = "My App"3. Forgetting dependencies:
// Bad: Forgetting to include signal in computedval fullName = computed { firstName.value + " " + lastName.value // Works but fragile}
// Better: Make dependencies explicitval fullName = computed { "${firstName.value} ${lastName.value}"}4. Ignoring server-side constraints:
Server-side UI isn’t as instant as client-side React. You need to account for network round-trips.
Why Backend Developers Should Care
I think this matters for backend developers because:
1. Use your existing skills:
- Stay in Kotlin/Java
- Use dependency injection
- Type safety throughout
- No context switching to JavaScript
2. Reduce complexity:
- Single codebase
- No API layer duplication
- No frontend framework setup
3. Better maintainability:
- State changes propagate automatically
- Clear data flow
- Less code to write
4. Faster development: I can build a full-stack app without:
- Learning React/Vue
- Designing REST APIs
- Building frontend state management
Trade-offs
This approach isn’t perfect. The Reddit author mentioned these drawbacks:
Network dependency:
- Requires WebSocket connection
- Not great for offline-first apps
- Higher latency than client-side React
Server load:
- UI rendering happens on server
- Not distributed to clients like static JS
- Scalability concerns for high-traffic apps
Smaller ecosystem:
- Fewer libraries than React
- Smaller community
- Less job market demand
Summary
In this post, I showed how declarative UI patterns work on the server side and why they matter for backend developers. The key point is that by describing what your UI should look like rather than manually updating it, you eliminate entire classes of bugs related to state synchronization.
The Reddit developer’s implementation of reactive signals in Kotlin shows you can have React-like patterns without leaving the JVM ecosystem. No JavaScript required.
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:
- 👨💻 How I brought the declarative pattern to Vaadin with reactive signals in Kotlin
- 👨💻 Vaadin Framework
- 👨💻 React Official Documentation
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments