CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-jetbrains-compose-ui--ui-wasm-js

Compose Multiplatform UI library for WebAssembly/JS target - declarative framework for sharing UIs across multiple platforms with Kotlin.

Pending
Overview
Eval results
Files

state-management.mddocs/

State Management

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.

Local State Management

remember

Preserve values across recompositions.

@Composable
inline fun <T> remember(calculation: () -> T): T

@Composable  
inline fun <T> remember(key1: Any?, calculation: () -> T): T

@Composable
inline fun <T> remember(key1: Any?, key2: Any?, calculation: () -> T): T

@Composable
inline fun <T> remember(vararg keys: Any?, calculation: () -> T): T

Basic Usage:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    
    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

Keyed Remember:

@Composable
fun UserProfile(userId: String) {
    // Recalculate when userId changes
    val userInfo by remember(userId) {
        mutableStateOf(loadUserInfo(userId))
    }
    
    Text("User: ${userInfo?.name ?: "Loading..."}")
}

mutableStateOf

Create observable state that triggers recomposition.

fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T>

Usage Patterns:

@Composable
fun StateExamples() {
    // Simple state
    var text by remember { mutableStateOf("") }
    
    // Complex state
    var user by remember { 
        mutableStateOf(User(name = "", email = "")) 
    }
    
    // List state
    var items by remember { 
        mutableStateOf(listOf<String>()) 
    }
    
    // State with custom policy  
    var complexObject by remember {
        mutableStateOf(
            ComplexObject(),
            policy = referentialEqualityPolicy()
        )
    }
}

State Delegation

@Composable
fun StateDelegate() {
    var isExpanded by remember { mutableStateOf(false) }
    var selectedIndex by remember { mutableStateOf(0) }
    var inputText by remember { mutableStateOf("") }
    
    Column {
        TextField(
            value = inputText,
            onValueChange = { inputText = it },
            label = { Text("Enter text") }
        )
        
        Switch(
            checked = isExpanded,
            onCheckedChange = { isExpanded = it }
        )
        
        if (isExpanded) {
            LazyColumn {
                items(10) { index ->
                    ListItem(
                        modifier = Modifier.clickable {
                            selectedIndex = index
                        },
                        text = { Text("Item $index") },
                        backgroundColor = if (index == selectedIndex) {
                            MaterialTheme.colors.primary.copy(alpha = 0.1f)
                        } else Color.Transparent
                    )
                }
            }
        }
    }
}

Side Effects

LaunchedEffect

Execute suspend functions within composables.

@Composable
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
)

@Composable
fun LaunchedEffect(
    key1: Any?,
    key2: Any?, 
    block: suspend CoroutineScope.() -> Unit
)

@Composable
fun LaunchedEffect(
    vararg keys: Any?,
    block: suspend CoroutineScope.() -> Unit
)

Network Requests:

@Composable
fun DataLoader(userId: String) {
    var userData by remember { mutableStateOf<User?>(null) }
    var isLoading by remember { mutableStateOf(false) }
    var error by remember { mutableStateOf<String?>(null) }
    
    LaunchedEffect(userId) {
        isLoading = true
        error = null
        
        try {
            userData = fetchUser(userId)
        } catch (e: Exception) {
            error = e.message
        } finally {
            isLoading = false
        }
    }
    
    when {
        isLoading -> CircularProgressIndicator()
        error != null -> Text("Error: $error", color = MaterialTheme.colors.error)
        userData != null -> UserDisplay(userData!!)
        else -> Text("No data")
    }
}

Timers and Intervals:

@Composable
fun Timer() {
    var seconds by remember { mutableStateOf(0) }
    
    LaunchedEffect(Unit) {
        while (true) {
            delay(1000)
            seconds++
        }
    }
    
    Text("Elapsed: ${seconds}s")
}

DisposableEffect

Handle cleanup when composables leave the composition.

@Composable
fun DisposableEffect(
    key1: Any?,
    effect: DisposableEffectScope.() -> DisposableEffectResult
)

Event Listeners:

@Composable
fun WindowSizeTracker() {
    var windowSize by remember { 
        mutableStateOf(Size(window.innerWidth, window.innerHeight))
    }
    
    DisposableEffect(Unit) {
        val updateSize = {
            windowSize = Size(window.innerWidth, window.innerHeight)
        }
        
        window.addEventListener("resize", updateSize)
        
        onDispose {
            window.removeEventListener("resize", updateSize)
        }
    }
    
    Text("Window: ${windowSize.width}x${windowSize.height}")
}

Resource Management:

@Composable
fun MediaPlayer(mediaUrl: String) {
    DisposableEffect(mediaUrl) {
        val player = createMediaPlayer(mediaUrl)
        player.prepare()
        
        onDispose {
            player.release()
        }
    }
}

SideEffect

Execute code on every successful recomposition.

@Composable
fun SideEffect(effect: () -> Unit)

Usage:

@Composable
fun AnalyticsTracker(screenName: String) {
    SideEffect {
        // Runs after every recomposition
        logScreenView(screenName)
    }
}

Global State Management

State Hoisting

Lift state up to common ancestor composables.

@Composable
fun ParentComponent() {
    // Shared state at parent level
    var sharedValue by remember { mutableStateOf(0) }
    var isDialogOpen by remember { mutableStateOf(false) }
    
    Column {
        ChildA(
            value = sharedValue,
            onValueChange = { sharedValue = it }
        )
        
        ChildB(
            value = sharedValue,
            onShowDialog = { isDialogOpen = true }
        )
        
        if (isDialogOpen) {
            AlertDialog(
                onDismissRequest = { isDialogOpen = false },
                title = { Text("Shared Value") },
                text = { Text("Current value: $sharedValue") },
                buttons = {
                    TextButton(onClick = { isDialogOpen = false }) {
                        Text("OK")
                    }
                }
            )
        }
    }
}

@Composable
fun ChildA(
    value: Int,
    onValueChange: (Int) -> Unit
) {
    Button(onClick = { onValueChange(value + 1) }) {
        Text("Increment: $value")
    }
}

@Composable  
fun ChildB(
    value: Int,
    onShowDialog: () -> Unit
) {
    Button(onClick = onShowDialog) {
        Text("Show Value: $value")
    }
}

Application-Level State

// Global application state
object AppState {
    val isLoggedIn = mutableStateOf(false)
    val currentUser = mutableStateOf<User?>(null)
    val theme = mutableStateOf("light")
    val language = mutableStateOf("en")
}

@Composable
fun App() {
    val isLoggedIn by AppState.isLoggedIn
    val theme by AppState.theme
    
    MaterialTheme(
        colors = if (theme == "dark") darkColors() else lightColors()
    ) {
        if (isLoggedIn) {
            MainApp()
        } else {
            LoginScreen(
                onLoginSuccess = { user ->
                    AppState.currentUser.value = user
                    AppState.isLoggedIn.value = true
                }
            )
        }
    }
}

Data Flow Patterns

Flow Integration

Work with Kotlin Flow for reactive data streams.

@Composable
fun <T> Flow<T>.collectAsState(
    initial: T,
    context: CoroutineContext = EmptyCoroutineContext
): State<T>

Usage:

class DataRepository {
    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()
    
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
    
    suspend fun loadUsers() {
        _isLoading.value = true
        try {
            val loadedUsers = fetchUsersFromApi()
            _users.value = loadedUsers
        } finally {
            _isLoading.value = false
        }
    }
}

@Composable
fun UserList(repository: DataRepository) {
    val users by repository.users.collectAsState()
    val isLoading by repository.isLoading.collectAsState()
    
    LaunchedEffect(Unit) {
        repository.loadUsers()
    }
    
    if (isLoading) {
        CircularProgressIndicator()
    } else {
        LazyColumn {
            items(users) { user ->
                UserItem(user = user)
            }
        }
    }
}

ViewModel Pattern

State management with ViewModel-like pattern.

class ScreenViewModel {
    private val _uiState = MutableStateFlow(ScreenUiState())
    val uiState: StateFlow<ScreenUiState> = _uiState.asStateFlow()
    
    fun updateTitle(title: String) {
        _uiState.value = _uiState.value.copy(title = title)
    }
    
    fun loadData() {
        _uiState.value = _uiState.value.copy(isLoading = true)
        
        // Simulate async operation
        MainScope().launch {
            delay(2000)
            _uiState.value = _uiState.value.copy(
                isLoading = false,
                data = loadedData
            )
        }
    }
}

data class ScreenUiState(
    val title: String = "",
    val isLoading: Boolean = false,
    val data: List<String> = emptyList(),
    val error: String? = null
)

@Composable
fun Screen(viewModel: ScreenViewModel) {
    val uiState by viewModel.uiState.collectAsState()
    
    LaunchedEffect(Unit) {
        viewModel.loadData()
    }
    
    Column {
        TextField(
            value = uiState.title,
            onValueChange = viewModel::updateTitle,
            label = { Text("Title") }
        )
        
        when {
            uiState.isLoading -> CircularProgressIndicator()
            uiState.error != null -> Text("Error: ${uiState.error}")
            else -> {
                LazyColumn {
                    items(uiState.data) { item ->
                        Text(item)
                    }
                }
            }
        }
    }
}

State Persistence

Browser Storage Integration

@Composable
fun rememberPersistedState(
    key: String,
    defaultValue: String
): MutableState<String> {
    return remember(key) {
        val initialValue = window.localStorage.getItem(key) ?: defaultValue
        mutableStateOf(initialValue)
    }.also { state ->
        LaunchedEffect(state.value) {
            window.localStorage.setItem(key, state.value)
        }
    }
}

@Composable
fun PersistentSettings() {
    var username by rememberPersistedState("username", "")
    var theme by rememberPersistedState("theme", "light")
    
    Column {
        TextField(
            value = username,
            onValueChange = { username = it },
            label = { Text("Username") }
        )
        
        Switch(
            checked = theme == "dark",
            onCheckedChange = { isDark ->
                theme = if (isDark) "dark" else "light"
            }
        )
    }
}

Complex State Persistence

@Composable
fun <T> rememberPersistedComplexState(
    key: String,
    defaultValue: T,
    serializer: (T) -> String,
    deserializer: (String) -> T
): MutableState<T> {
    return remember(key) {
        val storedValue = window.localStorage.getItem(key)
        val initialValue = if (storedValue != null) {
            try {
                deserializer(storedValue)
            } catch (e: Exception) {
                defaultValue
            }
        } else {
            defaultValue
        }
        mutableStateOf(initialValue)
    }.also { state ->
        LaunchedEffect(state.value) {
            try {
                val serialized = serializer(state.value)
                window.localStorage.setItem(key, serialized)
            } catch (e: Exception) {
                console.error("Failed to persist state: $e")
            }
        }
    }
}

// Usage with JSON
@Composable
fun PersistentUserProfile() {
    var userProfile by rememberPersistedComplexState(
        key = "user_profile",
        defaultValue = UserProfile(name = "", email = ""),
        serializer = { Json.encodeToString(it) },
        deserializer = { Json.decodeFromString<UserProfile>(it) }
    )
    
    Column {
        TextField(
            value = userProfile.name,
            onValueChange = { userProfile = userProfile.copy(name = it) },
            label = { Text("Name") }
        )
        
        TextField(
            value = userProfile.email,
            onValueChange = { userProfile = userProfile.copy(email = it) },
            label = { Text("Email") }
        )
    }
}

Performance Optimization

State Scope Management

@Composable
fun OptimizedList(items: List<Item>) {
    // Avoid recreating derived state unnecessarily
    val filteredItems by remember(items) {
        derivedStateOf {
            items.filter { it.isVisible }
        }
    }
    
    val sortedItems by remember(filteredItems) {
        derivedStateOf {
            filteredItems.sortedBy { it.priority }
        }
    }
    
    LazyColumn {
        items(sortedItems) { item ->
            ItemRow(
                item = item,
                // Minimize recomposition scope
                onItemClick = remember(item.id) {
                    { handleItemClick(item.id) }
                }
            )
        }
    }
}

Stable Collections

@Stable
data class StableList<T>(
    private val list: List<T>
) : List<T> by list

@Composable
fun StableStateExample() {
    var items by remember { 
        mutableStateOf(StableList(emptyList<String>()))
    }
    
    // This won't cause unnecessary recompositions
    ItemList(items = items)
}

Error Handling

State Error Recovery

@Composable
fun RobustDataLoader() {
    var state by remember { 
        mutableStateOf<DataState>(DataState.Loading)
    }
    
    LaunchedEffect(Unit) {
        state = DataState.Loading
        
        try {
            val data = loadData()
            state = DataState.Success(data)
        } catch (e: Exception) {
            state = DataState.Error(e.message ?: "Unknown error")
        }
    }
    
    when (val currentState = state) {
        is DataState.Loading -> {
            CircularProgressIndicator()
        }
        is DataState.Success -> {
            DataDisplay(currentState.data)
        }
        is DataState.Error -> {
            Column {
                Text(
                    text = "Error: ${currentState.message}",
                    color = MaterialTheme.colors.error
                )
                Button(
                    onClick = { 
                        // Retry logic
                        state = DataState.Loading 
                    }
                ) {
                    Text("Retry")
                }
            }
        }
    }
}

sealed class DataState {
    object Loading : DataState()
    data class Success(val data: List<String>) : DataState()
    data class Error(val message: String) : DataState()
}

WASM-Specific Considerations

Memory Management

@Composable
fun MemoryEfficientState() {
    // Use derivedStateOf for computed values
    val expensiveComputation by remember {
        derivedStateOf {
            // Only recomputed when dependencies change
            performExpensiveCalculation()
        }
    }
    
    // Clean up large state objects
    DisposableEffect(Unit) {
        val largeObject = createLargeObject()
        
        onDispose {
            largeObject.dispose()
        }
    }
}

Single-Threaded Execution

@Composable
fun SingleThreadedStateManagement() {
    // All state updates happen on main thread
    var isProcessing by remember { mutableStateOf(false) }
    
    LaunchedEffect(Unit) {
        // Long-running operations should be yielding
        isProcessing = true
        
        repeat(1000) { index ->
            // Yield periodically to prevent blocking
            if (index % 100 == 0) {
                yield()
            }
            processItem(index)
        }
        
        isProcessing = false
    }
    
    if (isProcessing) {
        LinearProgressIndicator()
    }
}

Install with Tessl CLI

npx tessl i tessl/maven-org-jetbrains-compose-ui--ui-wasm-js

docs

browser-integration.md

index.md

material-design.md

resource-management.md

state-management.md

ui-components.md

window-management.md

tile.json