Compose Multiplatform UI library for WebAssembly/JS target - declarative framework for sharing UIs across multiple platforms with Kotlin.
—
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.
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): TBasic 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..."}")
}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()
)
}
}@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
)
}
}
}
}
}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")
}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()
}
}
}Execute code on every successful recomposition.
@Composable
fun SideEffect(effect: () -> Unit)Usage:
@Composable
fun AnalyticsTracker(screenName: String) {
SideEffect {
// Runs after every recomposition
logScreenView(screenName)
}
}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")
}
}// 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
}
)
}
}
}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)
}
}
}
}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)
}
}
}
}
}
}@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"
}
)
}
}@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") }
)
}
}@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
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)
}@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()
}@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()
}
}
}@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