Skip to content

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:

UserView.kt - Imperative approach
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 update
nameLabel.text = user.name
editButton.isEnabled = user.canEdit

You write:

// Declarative: WHAT to show
val 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:

Signal.kt - Core reactive primitives
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 syntax
var Signal<T>.value: T
get() = get()
set(value) = set(value)

Then they added computed() for derived state:

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

Effect.kt - Side effects
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:

UserViewDeclarative.kt - Declarative approach
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):

  1. user.value changes
  2. All computed signals that depend on user recalculate
  3. Effects that depend on those computed values re-run
  4. 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:

TodoApp.kt - Complete declarative example
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:

Mistake.kt - Don't mix styles
// Bad: Imperative listener with declarative signals
button.onClick {
user.value = newUser
nameLabel.refresh() // Don't need this!
}

2. Over-computing:

Overcompute.kt - Don't overuse computed
// Bad: Computed for static values
val appName = computed { "My App" } // Never changes
// Good: Just use a constant
val appName = "My App"

3. Forgetting dependencies:

MissingDep.kt - Missing dependency bug
// Bad: Forgetting to include signal in computed
val fullName = computed {
firstName.value + " " + lastName.value // Works but fragile
}
// Better: Make dependencies explicit
val 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:

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

Comments