0
# State Management
1
2
State management in Compose Multiplatform for WASM/JS leverages the Compose runtime system to provide reactive programming patterns that work seamlessly with the WASM execution model. This includes local component state, global application state, side effects, and data flow patterns optimized for web deployment.
3
4
## Local State Management
5
6
### remember
7
8
Preserve values across recompositions.
9
10
```kotlin { .api }
11
@Composable
12
inline fun <T> remember(calculation: () -> T): T
13
14
@Composable
15
inline fun <T> remember(key1: Any?, calculation: () -> T): T
16
17
@Composable
18
inline fun <T> remember(key1: Any?, key2: Any?, calculation: () -> T): T
19
20
@Composable
21
inline fun <T> remember(vararg keys: Any?, calculation: () -> T): T
22
```
23
24
**Basic Usage:**
25
```kotlin { .api }
26
@Composable
27
fun Counter() {
28
var count by remember { mutableStateOf(0) }
29
30
Column {
31
Text("Count: $count")
32
Button(onClick = { count++ }) {
33
Text("Increment")
34
}
35
}
36
}
37
```
38
39
**Keyed Remember:**
40
```kotlin { .api }
41
@Composable
42
fun UserProfile(userId: String) {
43
// Recalculate when userId changes
44
val userInfo by remember(userId) {
45
mutableStateOf(loadUserInfo(userId))
46
}
47
48
Text("User: ${userInfo?.name ?: "Loading..."}")
49
}
50
```
51
52
### mutableStateOf
53
54
Create observable state that triggers recomposition.
55
56
```kotlin { .api }
57
fun <T> mutableStateOf(
58
value: T,
59
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
60
): MutableState<T>
61
```
62
63
**Usage Patterns:**
64
```kotlin { .api }
65
@Composable
66
fun StateExamples() {
67
// Simple state
68
var text by remember { mutableStateOf("") }
69
70
// Complex state
71
var user by remember {
72
mutableStateOf(User(name = "", email = ""))
73
}
74
75
// List state
76
var items by remember {
77
mutableStateOf(listOf<String>())
78
}
79
80
// State with custom policy
81
var complexObject by remember {
82
mutableStateOf(
83
ComplexObject(),
84
policy = referentialEqualityPolicy()
85
)
86
}
87
}
88
```
89
90
### State Delegation
91
92
```kotlin { .api }
93
@Composable
94
fun StateDelegate() {
95
var isExpanded by remember { mutableStateOf(false) }
96
var selectedIndex by remember { mutableStateOf(0) }
97
var inputText by remember { mutableStateOf("") }
98
99
Column {
100
TextField(
101
value = inputText,
102
onValueChange = { inputText = it },
103
label = { Text("Enter text") }
104
)
105
106
Switch(
107
checked = isExpanded,
108
onCheckedChange = { isExpanded = it }
109
)
110
111
if (isExpanded) {
112
LazyColumn {
113
items(10) { index ->
114
ListItem(
115
modifier = Modifier.clickable {
116
selectedIndex = index
117
},
118
text = { Text("Item $index") },
119
backgroundColor = if (index == selectedIndex) {
120
MaterialTheme.colors.primary.copy(alpha = 0.1f)
121
} else Color.Transparent
122
)
123
}
124
}
125
}
126
}
127
}
128
```
129
130
## Side Effects
131
132
### LaunchedEffect
133
134
Execute suspend functions within composables.
135
136
```kotlin { .api }
137
@Composable
138
fun LaunchedEffect(
139
key1: Any?,
140
block: suspend CoroutineScope.() -> Unit
141
)
142
143
@Composable
144
fun LaunchedEffect(
145
key1: Any?,
146
key2: Any?,
147
block: suspend CoroutineScope.() -> Unit
148
)
149
150
@Composable
151
fun LaunchedEffect(
152
vararg keys: Any?,
153
block: suspend CoroutineScope.() -> Unit
154
)
155
```
156
157
**Network Requests:**
158
```kotlin { .api }
159
@Composable
160
fun DataLoader(userId: String) {
161
var userData by remember { mutableStateOf<User?>(null) }
162
var isLoading by remember { mutableStateOf(false) }
163
var error by remember { mutableStateOf<String?>(null) }
164
165
LaunchedEffect(userId) {
166
isLoading = true
167
error = null
168
169
try {
170
userData = fetchUser(userId)
171
} catch (e: Exception) {
172
error = e.message
173
} finally {
174
isLoading = false
175
}
176
}
177
178
when {
179
isLoading -> CircularProgressIndicator()
180
error != null -> Text("Error: $error", color = MaterialTheme.colors.error)
181
userData != null -> UserDisplay(userData!!)
182
else -> Text("No data")
183
}
184
}
185
```
186
187
**Timers and Intervals:**
188
```kotlin { .api }
189
@Composable
190
fun Timer() {
191
var seconds by remember { mutableStateOf(0) }
192
193
LaunchedEffect(Unit) {
194
while (true) {
195
delay(1000)
196
seconds++
197
}
198
}
199
200
Text("Elapsed: ${seconds}s")
201
}
202
```
203
204
### DisposableEffect
205
206
Handle cleanup when composables leave the composition.
207
208
```kotlin { .api }
209
@Composable
210
fun DisposableEffect(
211
key1: Any?,
212
effect: DisposableEffectScope.() -> DisposableEffectResult
213
)
214
```
215
216
**Event Listeners:**
217
```kotlin { .api }
218
@Composable
219
fun WindowSizeTracker() {
220
var windowSize by remember {
221
mutableStateOf(Size(window.innerWidth, window.innerHeight))
222
}
223
224
DisposableEffect(Unit) {
225
val updateSize = {
226
windowSize = Size(window.innerWidth, window.innerHeight)
227
}
228
229
window.addEventListener("resize", updateSize)
230
231
onDispose {
232
window.removeEventListener("resize", updateSize)
233
}
234
}
235
236
Text("Window: ${windowSize.width}x${windowSize.height}")
237
}
238
```
239
240
**Resource Management:**
241
```kotlin { .api }
242
@Composable
243
fun MediaPlayer(mediaUrl: String) {
244
DisposableEffect(mediaUrl) {
245
val player = createMediaPlayer(mediaUrl)
246
player.prepare()
247
248
onDispose {
249
player.release()
250
}
251
}
252
}
253
```
254
255
### SideEffect
256
257
Execute code on every successful recomposition.
258
259
```kotlin { .api }
260
@Composable
261
fun SideEffect(effect: () -> Unit)
262
```
263
264
**Usage:**
265
```kotlin { .api }
266
@Composable
267
fun AnalyticsTracker(screenName: String) {
268
SideEffect {
269
// Runs after every recomposition
270
logScreenView(screenName)
271
}
272
}
273
```
274
275
## Global State Management
276
277
### State Hoisting
278
279
Lift state up to common ancestor composables.
280
281
```kotlin { .api }
282
@Composable
283
fun ParentComponent() {
284
// Shared state at parent level
285
var sharedValue by remember { mutableStateOf(0) }
286
var isDialogOpen by remember { mutableStateOf(false) }
287
288
Column {
289
ChildA(
290
value = sharedValue,
291
onValueChange = { sharedValue = it }
292
)
293
294
ChildB(
295
value = sharedValue,
296
onShowDialog = { isDialogOpen = true }
297
)
298
299
if (isDialogOpen) {
300
AlertDialog(
301
onDismissRequest = { isDialogOpen = false },
302
title = { Text("Shared Value") },
303
text = { Text("Current value: $sharedValue") },
304
buttons = {
305
TextButton(onClick = { isDialogOpen = false }) {
306
Text("OK")
307
}
308
}
309
)
310
}
311
}
312
}
313
314
@Composable
315
fun ChildA(
316
value: Int,
317
onValueChange: (Int) -> Unit
318
) {
319
Button(onClick = { onValueChange(value + 1) }) {
320
Text("Increment: $value")
321
}
322
}
323
324
@Composable
325
fun ChildB(
326
value: Int,
327
onShowDialog: () -> Unit
328
) {
329
Button(onClick = onShowDialog) {
330
Text("Show Value: $value")
331
}
332
}
333
```
334
335
### Application-Level State
336
337
```kotlin { .api }
338
// Global application state
339
object AppState {
340
val isLoggedIn = mutableStateOf(false)
341
val currentUser = mutableStateOf<User?>(null)
342
val theme = mutableStateOf("light")
343
val language = mutableStateOf("en")
344
}
345
346
@Composable
347
fun App() {
348
val isLoggedIn by AppState.isLoggedIn
349
val theme by AppState.theme
350
351
MaterialTheme(
352
colors = if (theme == "dark") darkColors() else lightColors()
353
) {
354
if (isLoggedIn) {
355
MainApp()
356
} else {
357
LoginScreen(
358
onLoginSuccess = { user ->
359
AppState.currentUser.value = user
360
AppState.isLoggedIn.value = true
361
}
362
)
363
}
364
}
365
}
366
```
367
368
## Data Flow Patterns
369
370
### Flow Integration
371
372
Work with Kotlin Flow for reactive data streams.
373
374
```kotlin { .api }
375
@Composable
376
fun <T> Flow<T>.collectAsState(
377
initial: T,
378
context: CoroutineContext = EmptyCoroutineContext
379
): State<T>
380
```
381
382
**Usage:**
383
```kotlin { .api }
384
class DataRepository {
385
private val _users = MutableStateFlow<List<User>>(emptyList())
386
val users: StateFlow<List<User>> = _users.asStateFlow()
387
388
private val _isLoading = MutableStateFlow(false)
389
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
390
391
suspend fun loadUsers() {
392
_isLoading.value = true
393
try {
394
val loadedUsers = fetchUsersFromApi()
395
_users.value = loadedUsers
396
} finally {
397
_isLoading.value = false
398
}
399
}
400
}
401
402
@Composable
403
fun UserList(repository: DataRepository) {
404
val users by repository.users.collectAsState()
405
val isLoading by repository.isLoading.collectAsState()
406
407
LaunchedEffect(Unit) {
408
repository.loadUsers()
409
}
410
411
if (isLoading) {
412
CircularProgressIndicator()
413
} else {
414
LazyColumn {
415
items(users) { user ->
416
UserItem(user = user)
417
}
418
}
419
}
420
}
421
```
422
423
### ViewModel Pattern
424
425
State management with ViewModel-like pattern.
426
427
```kotlin { .api }
428
class ScreenViewModel {
429
private val _uiState = MutableStateFlow(ScreenUiState())
430
val uiState: StateFlow<ScreenUiState> = _uiState.asStateFlow()
431
432
fun updateTitle(title: String) {
433
_uiState.value = _uiState.value.copy(title = title)
434
}
435
436
fun loadData() {
437
_uiState.value = _uiState.value.copy(isLoading = true)
438
439
// Simulate async operation
440
MainScope().launch {
441
delay(2000)
442
_uiState.value = _uiState.value.copy(
443
isLoading = false,
444
data = loadedData
445
)
446
}
447
}
448
}
449
450
data class ScreenUiState(
451
val title: String = "",
452
val isLoading: Boolean = false,
453
val data: List<String> = emptyList(),
454
val error: String? = null
455
)
456
457
@Composable
458
fun Screen(viewModel: ScreenViewModel) {
459
val uiState by viewModel.uiState.collectAsState()
460
461
LaunchedEffect(Unit) {
462
viewModel.loadData()
463
}
464
465
Column {
466
TextField(
467
value = uiState.title,
468
onValueChange = viewModel::updateTitle,
469
label = { Text("Title") }
470
)
471
472
when {
473
uiState.isLoading -> CircularProgressIndicator()
474
uiState.error != null -> Text("Error: ${uiState.error}")
475
else -> {
476
LazyColumn {
477
items(uiState.data) { item ->
478
Text(item)
479
}
480
}
481
}
482
}
483
}
484
}
485
```
486
487
## State Persistence
488
489
### Browser Storage Integration
490
491
```kotlin { .api }
492
@Composable
493
fun rememberPersistedState(
494
key: String,
495
defaultValue: String
496
): MutableState<String> {
497
return remember(key) {
498
val initialValue = window.localStorage.getItem(key) ?: defaultValue
499
mutableStateOf(initialValue)
500
}.also { state ->
501
LaunchedEffect(state.value) {
502
window.localStorage.setItem(key, state.value)
503
}
504
}
505
}
506
507
@Composable
508
fun PersistentSettings() {
509
var username by rememberPersistedState("username", "")
510
var theme by rememberPersistedState("theme", "light")
511
512
Column {
513
TextField(
514
value = username,
515
onValueChange = { username = it },
516
label = { Text("Username") }
517
)
518
519
Switch(
520
checked = theme == "dark",
521
onCheckedChange = { isDark ->
522
theme = if (isDark) "dark" else "light"
523
}
524
)
525
}
526
}
527
```
528
529
### Complex State Persistence
530
531
```kotlin { .api }
532
@Composable
533
fun <T> rememberPersistedComplexState(
534
key: String,
535
defaultValue: T,
536
serializer: (T) -> String,
537
deserializer: (String) -> T
538
): MutableState<T> {
539
return remember(key) {
540
val storedValue = window.localStorage.getItem(key)
541
val initialValue = if (storedValue != null) {
542
try {
543
deserializer(storedValue)
544
} catch (e: Exception) {
545
defaultValue
546
}
547
} else {
548
defaultValue
549
}
550
mutableStateOf(initialValue)
551
}.also { state ->
552
LaunchedEffect(state.value) {
553
try {
554
val serialized = serializer(state.value)
555
window.localStorage.setItem(key, serialized)
556
} catch (e: Exception) {
557
console.error("Failed to persist state: $e")
558
}
559
}
560
}
561
}
562
563
// Usage with JSON
564
@Composable
565
fun PersistentUserProfile() {
566
var userProfile by rememberPersistedComplexState(
567
key = "user_profile",
568
defaultValue = UserProfile(name = "", email = ""),
569
serializer = { Json.encodeToString(it) },
570
deserializer = { Json.decodeFromString<UserProfile>(it) }
571
)
572
573
Column {
574
TextField(
575
value = userProfile.name,
576
onValueChange = { userProfile = userProfile.copy(name = it) },
577
label = { Text("Name") }
578
)
579
580
TextField(
581
value = userProfile.email,
582
onValueChange = { userProfile = userProfile.copy(email = it) },
583
label = { Text("Email") }
584
)
585
}
586
}
587
```
588
589
## Performance Optimization
590
591
### State Scope Management
592
593
```kotlin { .api }
594
@Composable
595
fun OptimizedList(items: List<Item>) {
596
// Avoid recreating derived state unnecessarily
597
val filteredItems by remember(items) {
598
derivedStateOf {
599
items.filter { it.isVisible }
600
}
601
}
602
603
val sortedItems by remember(filteredItems) {
604
derivedStateOf {
605
filteredItems.sortedBy { it.priority }
606
}
607
}
608
609
LazyColumn {
610
items(sortedItems) { item ->
611
ItemRow(
612
item = item,
613
// Minimize recomposition scope
614
onItemClick = remember(item.id) {
615
{ handleItemClick(item.id) }
616
}
617
)
618
}
619
}
620
}
621
```
622
623
### Stable Collections
624
625
```kotlin { .api }
626
@Stable
627
data class StableList<T>(
628
private val list: List<T>
629
) : List<T> by list
630
631
@Composable
632
fun StableStateExample() {
633
var items by remember {
634
mutableStateOf(StableList(emptyList<String>()))
635
}
636
637
// This won't cause unnecessary recompositions
638
ItemList(items = items)
639
}
640
```
641
642
## Error Handling
643
644
### State Error Recovery
645
646
```kotlin { .api }
647
@Composable
648
fun RobustDataLoader() {
649
var state by remember {
650
mutableStateOf<DataState>(DataState.Loading)
651
}
652
653
LaunchedEffect(Unit) {
654
state = DataState.Loading
655
656
try {
657
val data = loadData()
658
state = DataState.Success(data)
659
} catch (e: Exception) {
660
state = DataState.Error(e.message ?: "Unknown error")
661
}
662
}
663
664
when (val currentState = state) {
665
is DataState.Loading -> {
666
CircularProgressIndicator()
667
}
668
is DataState.Success -> {
669
DataDisplay(currentState.data)
670
}
671
is DataState.Error -> {
672
Column {
673
Text(
674
text = "Error: ${currentState.message}",
675
color = MaterialTheme.colors.error
676
)
677
Button(
678
onClick = {
679
// Retry logic
680
state = DataState.Loading
681
}
682
) {
683
Text("Retry")
684
}
685
}
686
}
687
}
688
}
689
690
sealed class DataState {
691
object Loading : DataState()
692
data class Success(val data: List<String>) : DataState()
693
data class Error(val message: String) : DataState()
694
}
695
```
696
697
## WASM-Specific Considerations
698
699
### Memory Management
700
701
```kotlin { .api }
702
@Composable
703
fun MemoryEfficientState() {
704
// Use derivedStateOf for computed values
705
val expensiveComputation by remember {
706
derivedStateOf {
707
// Only recomputed when dependencies change
708
performExpensiveCalculation()
709
}
710
}
711
712
// Clean up large state objects
713
DisposableEffect(Unit) {
714
val largeObject = createLargeObject()
715
716
onDispose {
717
largeObject.dispose()
718
}
719
}
720
}
721
```
722
723
### Single-Threaded Execution
724
725
```kotlin { .api }
726
@Composable
727
fun SingleThreadedStateManagement() {
728
// All state updates happen on main thread
729
var isProcessing by remember { mutableStateOf(false) }
730
731
LaunchedEffect(Unit) {
732
// Long-running operations should be yielding
733
isProcessing = true
734
735
repeat(1000) { index ->
736
// Yield periodically to prevent blocking
737
if (index % 100 == 0) {
738
yield()
739
}
740
processItem(index)
741
}
742
743
isProcessing = false
744
}
745
746
if (isProcessing) {
747
LinearProgressIndicator()
748
}
749
}
750
```