Summon provides flexible state management options that work across both JavaScript and JVM platforms, allowing you to build reactive UIs with ease.
Use the standalone state implementation for component-local state:
// Using the standalone Summon implementation - no imports needed
@Composable
fun Counter(): String {
// Create a state variable
val count = remember { mutableStateOf(0) }
return Column(
modifier = Modifier().padding("16px").gap("8px")
) {
Text("Count: ${count.value}") +
Button(
text = "Increment",
modifier = Modifier()
.backgroundColor("#0077cc")
.color("white")
.padding("8px 16px")
.borderRadius("4px")
.cursor("pointer")
.onClick("incrementCounter()")
)
}
}
// The remember function preserves state across recompositions in your JavaScript code
// Example JavaScript for interactivity:
// let counterValue = 0;
// function incrementCounter() {
// counterValue++;
// document.getElementById('app').innerHTML = Counter();
// }
The remember function preserves state, and you can implement reactivity through JavaScript event handlers.
Create derived state using computed values:
@Composable
fun TemperatureConverter(): String {
val celsius = remember { mutableStateOf(0) }
// Derived state - computed from celsius
val fahrenheit = (celsius.value * 9/5) + 32
return Column(
modifier = Modifier().padding("16px").gap("8px")
) {
Text("Celsius: ${celsius.value}°C") +
Text("Fahrenheit: ${fahrenheit}°F") +
Button(
text = "Increase Temperature",
modifier = Modifier()
.backgroundColor("#0077cc")
.color("white")
.padding("8px 16px")
.borderRadius("4px")
.cursor("pointer")
.onClick("increaseTemperature()")
)
}
}
// Example JavaScript for managing derived state:
// let celsiusValue = 0;
// function increaseTemperature() {
// celsiusValue++;
// updateTemperatureDisplay();
// }
// function updateTemperatureDisplay() {
// const fahrenheit = (celsiusValue * 9/5) + 32;
// document.getElementById('app').innerHTML = TemperatureConverter();
// }
Derived state can be computed directly from other state values, with JavaScript handling the updates.
The new simpleDerivedStateOf function provides a more straightforward way to create derived state:
@Composable
fun ShoppingCart() {
val items = remember { mutableStateListOf<Item>() }
// Automatically recalculates when items change
val totalPrice = simpleDerivedStateOf {
items.sumOf { it.price }
}
val itemCount = simpleDerivedStateOf {
items.size
}
Column {
Text("Items: ${itemCount.value}")
Text("Total: $${totalPrice.value}")
Button(
text = "Add Item",
onClick = {
items.add(Item("Product", 9.99))
}
)
}
}
Use produceState to create state from asynchronous operations:
@Composable
fun UserProfile(userId: String) {
val userProfile = produceState<UserProfile?>(
initialValue = null,
key1 = userId
) {
value = fetchUserProfile(userId)
}
when (val profile = userProfile.value) {
null -> Text("Loading...")
else -> Text("Welcome, ${profile.name}")
}
}
Convert Kotlin Flow to Summon State with collectAsState:
@Composable
fun LiveDataDisplay(dataFlow: Flow<String>) {
val currentData = dataFlow.collectAsState()
Text("Current value: ${currentData.value}")
}
Use mutableStateListOf for lists that trigger recomposition on modification:
@Composable
fun TodoList() {
val todos = remember { mutableStateListOf<String>() }
Column {
todos.forEach { todo ->
Row {
Text(todo)
Button(
text = "Remove",
onClick = { todos.remove(todo) }
)
}
}
Button(
text = "Add Todo",
onClick = { todos.add("New Todo ${todos.size + 1}") }
)
}
}
For sharing state between multiple components, "lift" the state to a common parent:
@Composable
fun ParentComponent(): String {
// State is declared in the parent - managed via JavaScript
val sharedValue = remember { mutableStateOf("") }
return Column(
modifier = Modifier().gap("16px")
) {
ChildInput(sharedValue.value) +
ChildDisplay(sharedValue.value)
}
}
@Composable
fun ChildInput(value: String): String {
return TextField(
value = value,
placeholder = "Enter a value",
modifier = Modifier()
.style("width", "200px")
.padding("8px")
.style("border", "1px solid #ccc")
.borderRadius("4px")
.attribute("oninput", "updateSharedValue(this.value)")
)
}
@Composable
fun ChildDisplay(value: String): String {
return Text(
text = "Current value: $value",
modifier = Modifier()
.padding("8px")
.backgroundColor("#f0f0f0")
.borderRadius("4px")
)
}
// Example JavaScript for state hoisting:
// let sharedState = "";
// function updateSharedValue(newValue) {
// sharedState = newValue;
// document.getElementById('app').innerHTML = ParentComponent();
// }
For more complex scenarios, use StateFlow for observable state:
// Composable annotation using standalone implementation
// State management using standalone implementation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
// Create a shared state holder class
class AppState {
private val _counter = MutableStateFlow(0)
val counter = _counter.asStateFlow()
fun increment() {
_counter.value++
}
fun decrement() {
_counter.value--
}
}
// Create a singleton instance
val appState = AppState()
// Use in components
@Composable
fun CounterDisplay() {
// Collect the StateFlow as state
val count by appState.counter.collectAsState()
Text("Count: $count")
}
@Composable
fun CounterControls() {
Row(
modifier = Modifier.gap(8.px)
) {
Button(
text = "Increment",
onClick = { appState.increment() }
)
Button(
text = "Decrement",
onClick = { appState.decrement() }
)
}
}
The collectAsState function converts a StateFlow into a state object that can be used in components and causes recomposition when the flow's value changes.
For more structured state management, use a state container:
// Composable annotation using standalone implementation
// State management using standalone implementation
// Define state and actions
data class TodoState(
val items: List<TodoItem> = emptyList(),
val newItemText: String = "",
val filter: TodoFilter = TodoFilter.ALL
)
enum class TodoFilter { ALL, ACTIVE, COMPLETED }
data class TodoItem(
val id: String,
val text: String,
val completed: Boolean
)
// Define actions
sealed class TodoAction {
data class SetNewItemText(val text: String) : TodoAction()
object AddItem : TodoAction()
data class ToggleItem(val id: String) : TodoAction()
data class RemoveItem(val id: String) : TodoAction()
data class SetFilter(val filter: TodoFilter) : TodoAction()
}
// Create a reducer function
fun todoReducer(state: TodoState, action: TodoAction): TodoState {
return when (action) {
is TodoAction.SetNewItemText -> state.copy(newItemText = action.text)
is TodoAction.AddItem -> {
if (state.newItemText.isBlank()) return state
val newItem = TodoItem(
id = UUID.randomUUID().toString(),
text = state.newItemText,
completed = false
)
state.copy(
items = state.items + newItem,
newItemText = ""
)
}
is TodoAction.ToggleItem -> {
val updatedItems = state.items.map { item ->
if (item.id == action.id) {
item.copy(completed = !item.completed)
} else {
item
}
}
state.copy(items = updatedItems)
}
is TodoAction.RemoveItem -> {
val updatedItems = state.items.filter { it.id != action.id }
state.copy(items = updatedItems)
}
is TodoAction.SetFilter -> state.copy(filter = action.filter)
}
}
// Create a store
val todoStore = createStore(TodoState(), ::todoReducer)
// Use in components
@Composable
fun TodoApp() {
val state by todoStore.state.collectAsState()
Column(
modifier = Modifier.padding(16.px).gap(16.px)
) {
// Add new todo
Row(
modifier = Modifier.gap(8.px)
) {
TextField(
value = state.newItemText,
onValueChange = { todoStore.dispatch(TodoAction.SetNewItemText(it)) },
placeholder = "Add a new todo"
)
Button(
text = "Add",
onClick = { todoStore.dispatch(TodoAction.AddItem) }
)
}
// Filter controls
Row(
modifier = Modifier.gap(8.px)
) {
TodoFilter.values().forEach { filter ->
Button(
text = filter.name,
onClick = { todoStore.dispatch(TodoAction.SetFilter(filter)) },
modifier = Modifier.applyIf(state.filter == filter) {
backgroundColor("#0077cc").color("#ffffff")
}
)
}
}
// Todo list
val filteredItems = when (state.filter) {
TodoFilter.ALL -> state.items
TodoFilter.ACTIVE -> state.items.filter { !it.completed }
TodoFilter.COMPLETED -> state.items.filter { it.completed }
}
Column(
modifier = Modifier.gap(8.px)
) {
filteredItems.forEach { item ->
Row(
modifier = Modifier
.padding(8.px)
.gap(8.px)
.alignItems(AlignItems.Center)
) {
Checkbox(
checked = item.completed,
onCheckedChange = { todoStore.dispatch(TodoAction.ToggleItem(item.id)) }
)
Text(
text = item.text,
modifier = Modifier.applyIf(item.completed) {
textDecoration(TextDecoration.LineThrough)
}
)
Button(
text = "Delete",
onClick = { todoStore.dispatch(TodoAction.RemoveItem(item.id)) }
)
}
}
}
}
}
This pattern provides a predictable state container with unidirectional data flow, similar to Redux in the React ecosystem.
Summon provides cross-platform persistence for state:
// State management using standalone implementation
// Define persisted state
data class UserPreferences(
val theme: String = "light",
val fontSize: Int = 16
)
// Create a persisted state container
val userPreferences = persistentStateOf(
"user_preferences", // Storage key
UserPreferences(), // Default value
UserPreferences::class // Class reference for serialization
)
// Use in components
class SettingsComponent : Composable {
override fun render() {
var preferences by userPreferences
Column(
modifier = Modifier.padding(16.px).gap(16.px)
) {
// Theme selector
Row(
modifier = Modifier.gap(8.px)
) {
Text("Theme:")
Button(
text = "Light",
onClick = {
preferences = preferences.copy(theme = "light")
},
modifier = Modifier.opacity(
if (preferences.theme == "light") 1.0 else 0.5
)
)
Button(
text = "Dark",
onClick = {
preferences = preferences.copy(theme = "dark")
},
modifier = Modifier.opacity(
if (preferences.theme == "dark") 1.0 else 0.5
)
)
}
// Font size selector
Row(
modifier = Modifier.gap(8.px).alignItems(AlignItems.Center)
) {
Text("Font Size:")
Button(
text = "-",
onClick = {
preferences = preferences.copy(
fontSize = (preferences.fontSize - 1).coerceAtLeast(12)
)
}
)
Text("${preferences.fontSize}px")
Button(
text = "+",
onClick = {
preferences = preferences.copy(
fontSize = (preferences.fontSize + 1).coerceAtMost(24)
)
}
)
}
}
}
}
The persistentStateOf function creates a state that is automatically saved to local storage on the JS platform and preferences/properties on the JVM platform.
Summon supports the ViewModel pattern for managing component state:
// ViewModel using standalone implementation
// State management using standalone implementation
import kotlinx.coroutines.flow.StateFlow
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
fun increment() {
_count.value++
}
fun decrement() {
_count.value--
}
fun loadData() {
viewModelScope.launch {
_isLoading.value = true
try {
// Simulate async operation
delay(1000)
_count.value = 42
} finally {
_isLoading.value = false
}
}
}
}
@Composable
fun CounterScreen() {
val viewModel = rememberViewModel { CounterViewModel() }
val count by viewModel.count.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
Column(modifier = Modifier.padding(16.px)) {
if (isLoading) {
Progress()
} else {
Text("Count: $count")
Row(modifier = Modifier.gap(8.px)) {
Button("Increment") { viewModel.increment() }
Button("Decrement") { viewModel.decrement() }
Button("Load Data") { viewModel.loadData() }
}
}
}
}
For state that should survive configuration changes and process death:
@Composable
fun FormScreen() {
// State that survives configuration changes
var name by rememberSaveable { mutableStateOf("") }
var email by rememberSaveable { mutableStateOf("") }
var agreed by rememberSaveable { mutableStateOf(false) }
// Complex state with custom saver
var formData by rememberSaveable(
saver = FormDataSaver
) { mutableStateOf(FormData()) }
Column {
TextField(
value = name,
onValueChange = { name = it },
placeholder = "Name"
)
TextField(
value = email,
onValueChange = { email = it },
placeholder = "Email"
)
Checkbox(
checked = agreed,
onCheckedChange = { agreed = it },
label = "I agree to terms"
)
}
}
// Custom saver for complex objects
object FormDataSaver : Saver<FormData, Bundle> {
override fun save(value: FormData): Bundle = Bundle().apply {
putString("name", value.name)
putString("email", value.email)
putBoolean("agreed", value.agreed)
}
override fun restore(value: Bundle): FormData = FormData(
name = value.getString("name", ""),
email = value.getString("email", ""),
agreed = value.getBoolean("agreed", false)
)
}
Seamless integration with Kotlin Flow for reactive programming:
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
// Convert state to flow
val queryFlow = snapshotFlow { query }
// Debounced search
val searchResults by queryFlow
.debounce(300)
.flatMapLatest { searchQuery ->
if (searchQuery.isBlank()) {
flowOf(emptyList())
} else {
searchRepository.search(searchQuery)
}
}
.collectAsState(initial = emptyList())
Column {
TextField(
value = query,
onValueChange = { query = it },
placeholder = "Search..."
)
LazyColumn {
items(searchResults) { result ->
SearchResultItem(result)
}
}
}
}
// Combine multiple flows
@Composable
fun DashboardScreen() {
val userFlow = userRepository.currentUser
val notificationsFlow = notificationRepository.unreadCount
val dashboardState by combine(
userFlow,
notificationsFlow
) { user, notificationCount ->
DashboardState(user, notificationCount)
}.collectAsState(initial = DashboardState.Loading)
when (dashboardState) {
is DashboardState.Loading -> Progress()
is DashboardState.Success -> {
Column {
Text("Welcome, ${dashboardState.user.name}")
Badge("${dashboardState.notificationCount}")
}
}
}
}
Advanced patterns for managing state across component hierarchies:
// State holder class
class FormState(
initialValues: FormValues = FormValues()
) {
var values by mutableStateOf(initialValues)
private set
var errors by mutableStateOf<Map<String, String>>(emptyMap())
private set
val isValid: Boolean
get() = errors.isEmpty() && values.isComplete()
fun updateField(field: String, value: String) {
values = values.copy(field to value)
validateField(field, value)
}
private fun validateField(field: String, value: String) {
val error = when (field) {
"email" -> if (!value.contains("@")) "Invalid email" else null
"password" -> if (value.length < 8) "Too short" else null
else -> null
}
errors = if (error != null) {
errors + (field to error)
} else {
errors - field
}
}
}
@Composable
fun rememberFormState(
initialValues: FormValues = FormValues()
): FormState = remember {
FormState(initialValues)
}
// Usage
@Composable
fun RegistrationForm() {
val formState = rememberFormState()
Column {
FormField(
value = formState.values.email,
onValueChange = { formState.updateField("email", it) },
error = formState.errors["email"],
label = "Email"
)
FormField(
value = formState.values.password,
onValueChange = { formState.updateField("password", it) },
error = formState.errors["password"],
label = "Password",
type = "password"
)
Button(
text = "Register",
enabled = formState.isValid,
onClick = { submitForm(formState.values) }
)
}
}
Summon provides platform-specific state extensions:
// State management using standalone implementation
// Use browser-specific state
val windowSize by windowSizeState()
Column {
Text("Window width: ${windowSize.width}px")
Text("Window height: ${windowSize.height}px")
}
// Media query state
val isMobile by mediaQueryState("(max-width: 768px)")
if (isMobile) {
MobileLayout()
} else {
DesktopLayout()
}
// Online status
val isOnline by onlineState()
if (!isOnline) {
Banner("You are offline")
}
// State management using standalone implementation
// Use JVM-specific state
val systemProperties by systemPropertiesState()
Column {
Text("Java version: ${systemProperties["java.version"]}")
Text("OS name: ${systemProperties["os.name"]}")
}
// File system state
val diskSpace by diskSpaceState("/")
ProgressBar(
progress = diskSpace.used / diskSpace.total,
label = "Disk Usage"
)