This document provides detailed information about the effects APIs in the Summon library.
The Summon effects system allows you to manage side effects and lifecycle operations within your composable components. Effects are executed after composition and are useful for operations like initialization, cleanup, and integrating with non-composable code.
The base APIs for defining and managing effects.
package code.yousef.summon.effects
// Execute an effect after each successful composition
@Composable
fun effect(
effect: () -> Unit
)
// Execute an effect when composition is first created
@Composable
fun onMount(
effect: () -> Unit
)
// Execute an effect when composition is disposed
@Composable
fun onDispose(
effect: () -> Unit
)
// Execute an effect after composition when dependencies change
@Composable
fun effectWithDeps(
vararg dependencies: Any?,
effect: () -> Unit
)
// Execute an effect once after composition
@Composable
fun onMountWithCleanup(
effect: () -> (() -> Unit)?
)
// Execute an effect with dependencies and cleanup
@Composable
fun effectWithDepsAndCleanup(
vararg dependencies: Any?,
effect: () -> (() -> Unit)?
)
These functions provide the core effect system in Summon, allowing you to perform operations at specific points in the component lifecycle.
@Composable
fun UserProfile(userId: String) {
// State for user data
val userData = remember { mutableStateOf<UserData?>(null) }
val isLoading = remember { mutableStateOf(true) }
val error = remember { mutableStateOf<String?>(null) }
// Basic effect: runs after every composition
effect {
console.log("UserProfile recomposed")
}
// Mount effect: runs once when component is first rendered
onMount {
console.log("UserProfile mounted")
}
// Cleanup effect: runs when component is removed
onDispose {
console.log("UserProfile disposed")
}
// Effect with dependencies: runs when userId changes
effectWithDeps(userId) {
isLoading.value = true
error.value = null
fetchUserData(userId)
.then { user ->
userData.value = user
isLoading.value = false
}
.catch { err ->
error.value = err.message
isLoading.value = false
}
}
// Effect with cleanup: sets up and tears down a subscription
onMountWithCleanup {
// Setup: subscribe to user status updates
val subscription = userStatusService.subscribe(userId) { status ->
// Update UI when status changes
userData.value = userData.value?.copy(status = status)
}
// Return cleanup function
return@onMountWithCleanup {
// Cleanup: unsubscribe when component is removed
subscription.unsubscribe()
}
}
// Render UI based on state
when {
isLoading.value -> LoadingSpinner()
error.value != null -> ErrorDisplay(message = error.value!!)
userData.value != null -> UserInfo(user = userData.value!!)
else -> Text("No user data")
}
}
APIs for managing asynchronous side effects safely within the component lifecycle.
package code.yousef.summon.effects
// Launch a coroutine scoped to the composition
@Composable
fun launchEffect(
block: suspend CoroutineScope.() -> Unit
): Job
// Launch a coroutine with dependencies
@Composable
fun launchEffectWithDeps(
vararg dependencies: Any?,
block: suspend CoroutineScope.() -> Unit
): Job
// Execute an async effect with cleanup
@Composable
fun asyncEffect(
effect: () -> Promise<() -> Unit>
)
// Execute an async effect when dependencies change
@Composable
fun asyncEffectWithDeps(
vararg dependencies: Any?,
effect: () -> Promise<() -> Unit>
)
// Safe state updates with cancellation handling
@Composable
fun <T> updateStateAsync(
state: MutableState<T>,
block: suspend () -> T
): Job
These functions help manage asynchronous operations within the component lifecycle, ensuring they're properly cleaned up when the component is disposed.
@Composable
fun NewsComponent() {
val articles = remember { mutableStateOf<List<Article>>(emptyList()) }
val isRefreshing = remember { mutableStateOf(false) }
// Launch a coroutine scoped to the component
launchEffect {
// Initial data load
try {
val data = newsService.getLatestArticles()
articles.value = data
} catch (e: Exception) {
console.error("Failed to load articles: ${e.message}")
}
}
// Launch a periodic refresh with cleanup
launchEffectWithDeps(articles.value.size) {
// Only start auto-refresh if we have articles
if (articles.value.isNotEmpty()) {
while (isActive) {
delay(60_000) // Refresh every minute
isRefreshing.value = true
try {
val freshData = newsService.getLatestArticles()
articles.value = freshData
} catch (e: Exception) {
console.error("Refresh failed: ${e.message}")
} finally {
isRefreshing.value = false
}
}
}
}
// Alternative with Promise API
asyncEffect {
return@asyncEffect newsService.subscribeToUpdates()
.then { subscription ->
// Return cleanup function
return@then {
subscription.cancel()
}
}
}
// Safe state updates with cancellation
Button(
text = "Refresh",
onClick = {
updateStateAsync(articles) {
newsService.getLatestArticles()
}
}
)
// Render articles
Column {
if (isRefreshing.value) {
Text("Refreshing...")
}
for (article in articles.value) {
ArticleCard(article)
}
}
}
Pre-built effects for common scenarios.
package code.yousef.summon.effects
// Effect for document title
@Composable
fun useDocumentTitle(title: String)
// Effect for handling keyboard shortcuts
@Composable
fun useKeyboardShortcut(
key: String,
modifiers: Set<KeyModifier> = emptySet(),
handler: (KeyboardEvent) -> Unit
)
// Effect for interval timer
@Composable
fun useInterval(
delayMs: Int,
callback: () -> Unit
): IntervalControl
// Effect for timeout
@Composable
fun useTimeout(
delayMs: Int,
callback: () -> Unit
): TimeoutControl
// Effect for handling clicks outside a component
@Composable
fun useClickOutside(
elementRef: ElementRef,
handler: (MouseEvent) -> Unit
)
// Effect for window size
@Composable
fun useWindowSize(): WindowSize
// Effect for browser location/URL
@Composable
fun useLocation(): Location
// Effect for local storage
@Composable
fun <T> useLocalStorage(
key: String,
initialValue: T,
serializer: (T) -> String = { it.toString() },
deserializer: (String) -> T
): MutableState<T>
// Effect for media queries
@Composable
fun useMediaQuery(
query: String
): MutableState<Boolean>
These pre-built effects handle common UI and browser interactions, abstracting away the complexity of managing their lifecycle.
@Composable
fun ProfilePage() {
val isMenuOpen = remember { mutableStateOf(false) }
val menuRef = remember { ElementRef() }
val userName = remember { mutableStateOf("") }
// Update document title
useDocumentTitle("User Profile - ${userName.value}")
// Handle Escape key to close menu
useKeyboardShortcut(
key = "Escape",
handler = {
if (isMenuOpen.value) {
isMenuOpen.value = false
}
}
)
// Close menu when clicking outside
useClickOutside(menuRef) {
isMenuOpen.value = false
}
// Store user preferences in localStorage
val theme = useLocalStorage(
key = "user-theme",
initialValue = "light",
deserializer = { it }
)
// Respond to dark mode preference
val prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)")
// Interval for session check
val sessionChecker = useInterval(60_000) {
checkUserSession()
}
// Get window size for responsive design
val windowSize = useWindowSize()
val isMobile = windowSize.width < 768
// Auto-save form with timeout
val autoSave = useTimeout(3000) {
saveUserProfile()
}
// Restart timeout on input
TextField(
value = userName.value,
onValueChange = {
userName.value = it
autoSave.reset() // Restart the timeout
}
)
// Menu with click outside
if (isMenuOpen.value) {
Box(
modifier = Modifier.ref(menuRef)
) {
// Menu content
Column {
Button(
text = "Toggle Theme",
onClick = {
theme.value = if (theme.value == "light") "dark" else "light"
}
)
// Other menu items
}
}
}
// Responsive layout based on window size
if (isMobile) {
MobileLayout()
} else {
DesktopLayout()
}
// Helper functions for the component
fun checkUserSession() {
// Check if user session is still valid
}
fun saveUserProfile() {
// Save profile data
}
}
// Control interfaces
interface IntervalControl {
fun pause()
fun resume()
fun reset()
fun setDelay(delayMs: Int)
}
interface TimeoutControl {
fun cancel()
fun reset()
fun setDelay(delayMs: Int)
}
data class WindowSize(
val width: Int,
val height: Int
)
APIs for composing and combining effects.
package code.yousef.summon.effects
// Create a custom composable effect
fun <T> createEffect(
setup: () -> T,
callback: (T) -> (() -> Unit)?
): @Composable () -> T
// Combine multiple effects into one
fun combineEffects(
vararg effects: @Composable () -> Unit
): @Composable () -> Unit
// Create a conditional effect that only runs when condition is true
fun conditionalEffect(
condition: () -> Boolean,
effect: @Composable () -> Unit
): @Composable () -> Unit
// Create a debounced effect
fun <T> debouncedEffect(
delayMs: Int,
producer: () -> T,
effect: (T) -> Unit
): @Composable () -> Unit
// Create a throttled effect
fun <T> throttledEffect(
delayMs: Int,
producer: () -> T,
effect: (T) -> Unit
): @Composable () -> Unit
These functions allow you to create reusable, composed effects that encapsulate complex behaviors.
// Custom form validation effect
fun validateFormEffect(
emailState: MutableState<String>,
passwordState: MutableState<String>,
errorsState: MutableState<Map<String, String>>
): @Composable () -> Unit = createEffect(
setup = {
// Return the current values to watch
Triple(emailState.value, passwordState.value, errorsState)
},
callback = { (email, password, errorsState) ->
// Perform validation
val errors = mutableMapOf<String, String>()
if (!isValidEmail(email)) {
errors["email"] = "Invalid email format"
}
if (password.length < 8) {
errors["password"] = "Password must be at least 8 characters"
}
// Update errors state
errorsState.value = errors
// No cleanup needed
null
}
)
// Debounced search effect
fun searchEffect(
query: MutableState<String>,
results: MutableState<List<SearchResult>>
): @Composable () -> Unit = debouncedEffect(
delayMs = 300,
producer = { query.value },
effect = { searchQuery ->
if (searchQuery.isNotEmpty()) {
performSearch(searchQuery)
.then { searchResults ->
results.value = searchResults
}
} else {
results.value = emptyList()
}
}
)
// Usage in a component
@Composable
fun SearchComponent() {
val query = remember { mutableStateOf("") }
val results = remember { mutableStateOf<List<SearchResult>>(emptyList()) }
val isLoading = remember { mutableStateOf(false) }
// Use debounced search effect
searchEffect(query, results)()
// Loading indicator effect
effectWithDeps(query.value) {
if (query.value.isNotEmpty()) {
isLoading.value = true
// Set loading to false after search completes
// (assuming search takes less than 500ms)
setTimeout(500) {
isLoading.value = false
}
}
}
// Combine multiple effects
val combinedEffect = combineEffects(
// Log searches
{
effectWithDeps(query.value) {
if (query.value.isNotEmpty()) {
logSearchQuery(query.value)
}
}
},
// Update recent searches
{
effectWithDeps(results.value) {
if (results.value.isNotEmpty() && query.value.isNotEmpty()) {
updateRecentSearches(query.value)
}
}
}
)
// Apply combined effect
combinedEffect()
// UI rendering
Column {
TextField(
value = query.value,
onValueChange = { query.value = it },
placeholder = "Search..."
)
if (isLoading.value) {
LoadingIndicator()
} else {
SearchResults(results = results.value)
}
}
}
Effects that utilize platform-specific APIs.
package code.yousef.summon.effects.browser
// Effect for browser history
@Composable
fun useHistory(): History
// Effect for browser navigator
@Composable
fun useNavigator(): Navigator
// Effect for IntersectionObserver
@Composable
fun useIntersectionObserver(
elementRef: ElementRef,
options: IntersectionObserverOptions = IntersectionObserverOptions()
): IntersectionState
// Effect for ResizeObserver
@Composable
fun useResizeObserver(
elementRef: ElementRef,
callback: (ResizeObserverEntry) -> Unit
): ResizeObserverCleanup
// Effect for online/offline status
@Composable
fun useOnlineStatus(): MutableState<Boolean>
// Effect for clipboard API (v0.2.8+: Full implementation with fallback)
@Composable
fun useClipboard(): ClipboardAPI
// Effect for geolocation
@Composable
fun useGeolocation(
options: GeolocationOptions = GeolocationOptions()
): GeolocationState
// Web animation API
@Composable
fun useWebAnimation(
elementRef: ElementRef,
keyframes: Array<Keyframe>,
options: AnimationOptions
): WebAnimationAPI
package code.yousef.summon.effects.jvm
// Effect for file system watcher
@Composable
fun useFileWatcher(
path: String,
callback: (FileEvent) -> Unit
): FileWatcherControl
// Effect for system tray
@Composable
fun useSystemTray(
icon: Image,
tooltip: String
): SystemTrayControl
// Effect for clipboard API (v0.2.8+: Full implementation with fallback)
@Composable
fun useClipboard(): ClipboardAPI
// Effect for screen information
@Composable
fun useScreenInfo(): ScreenInfo
Platform-specific effects provide access to capabilities unique to each platform while maintaining a consistent API pattern.
// JavaScript example
@Composable
fun LazyLoadComponent() {
val elementRef = remember { ElementRef() }
val isVisible = remember { mutableStateOf(false) }
val imageUrl = remember { mutableStateOf<String?>(null) }
// Track element visibility using IntersectionObserver
runJsOnly {
val intersection = useIntersectionObserver(
elementRef = elementRef,
options = IntersectionObserverOptions(
threshold = 0.1f,
rootMargin = "20px"
)
)
// Update visibility state
isVisible.value = intersection.isIntersecting
// Load image when element becomes visible
effectWithDeps(isVisible.value) {
if (isVisible.value && imageUrl.value == null) {
imageUrl.value = "https://example.com/image.jpg"
}
}
// Clipboard API (v0.2.8+: Browser API with automatic fallback)
val clipboard = useClipboard()
Button(
text = "Copy URL",
onClick = {
imageUrl.value?.let {
clipboard.writeText(it)
// v0.2.8+: Automatically uses navigator.clipboard API if available,
// falls back to document.execCommand for older browsers
}
}
)
// v0.2.8+: Read from clipboard (requires permissions)
Button(
text = "Paste",
onClick = {
clipboard.readText { pastedText ->
// Handle pasted text
console.log("Pasted: $pastedText")
}
}
)
// Online status
val isOnline = useOnlineStatus()
if (!isOnline.value) {
Text("You are offline. Some features may not work.")
}
}
// Element to observe
Box(
modifier = Modifier
.height(300.dp)
.width(100.pct)
.ref(elementRef)
) {
if (imageUrl.value != null) {
Image(src = imageUrl.value!!)
} else {
LoadingPlaceholder()
}
}
}
// JVM example
@Composable
fun DesktopComponent() {
val notificationCount = remember { mutableStateOf(0) }
// Only run on JVM platform
runJvmOnly {
// System tray integration
val systemTray = useSystemTray(
icon = loadImage("app_icon.png"),
tooltip = "My Desktop App"
)
// Update tray icon when notification count changes
effectWithDeps(notificationCount.value) {
if (notificationCount.value > 0) {
systemTray.displayNotification(
title = "New Notifications",
message = "You have ${notificationCount.value} new notifications"
)
systemTray.setIcon(loadImage("app_icon_notification.png"))
} else {
systemTray.setIcon(loadImage("app_icon.png"))
}
}
// Watch config file for changes
val fileWatcher = useFileWatcher("config.json") { event ->
when (event.type) {
FileEventType.MODIFIED -> reloadConfig()
FileEventType.DELETED -> createDefaultConfig()
else -> { /* ignore */ }
}
}
// Screen information for responsive layouts
val screenInfo = useScreenInfo()
if (screenInfo.dpi > 200) {
HighResolutionLayout()
} else {
StandardLayout()
}
}
// Regular UI rendering
Column {
Text("Notification Count: ${notificationCount.value}")
Button(
text = "Add Notification",
onClick = { notificationCount.value++ }
)
}
// Helper functions
fun reloadConfig() {
// Reload application configuration
}
fun createDefaultConfig() {
// Create default configuration file
}
}
Cross-platform WebSocket implementation with auto-reconnection and lifecycle management.
// WebSocket configuration
data class WebSocketConfig(
val url: String,
val protocols: List<String> = emptyList(),
val autoReconnect: Boolean = false,
val reconnectDelay: Long = 5000,
val maxReconnectAttempts: Int = -1, // -1 for unlimited
val onOpen: ((WebSocketClient) -> Unit)? = null,
val onMessage: ((String) -> Unit)? = null,
val onClose: ((code: Short, reason: String) -> Unit)? = null,
val onError: ((Throwable) -> Unit)? = null
)
// Create WebSocket client
expect class WebSocketClient {
fun connect(config: WebSocketConfig)
fun send(message: String)
fun close(code: Short = 1000, reason: String = "")
fun isConnected(): Boolean
}
// Factory function
expect fun createWebSocketClient(): WebSocketClient
Usage:
@Composable
fun ChatComponent() {
val webSocketClient = remember { mutableStateOf<WebSocketClient?>(null) }
val messages = remember { mutableStateOf(listOf<String>()) }
LaunchedEffect(Unit) {
val client = createWebSocketClient()
client.connect(WebSocketConfig(
url = "ws://localhost:8080/chat",
autoReconnect = true,
onMessage = { message ->
messages.value = messages.value + message
},
onError = { error ->
console.error("WebSocket error: ${error.message}")
}
))
webSocketClient.value = client
}
DisposableEffect(Unit) {
onDispose {
webSocketClient.value?.close()
}
}
}
Cross-platform HTTP client with comprehensive request/response handling.
// HTTP Request and Response
data class HttpRequest(
val url: String,
val method: HttpMethod = HttpMethod.GET,
val headers: Map<String, String> = emptyMap(),
val body: String? = null
)
data class HttpResponse(
val status: Int,
val statusText: String,
val headers: Map<String, String>,
val body: String
)
enum class HttpMethod {
GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
}
// HTTP Client interface
expect class HttpClient {
suspend fun execute(request: HttpRequest): HttpResponse
suspend fun get(url: String, headers: Map<String, String> = emptyMap()): HttpResponse
suspend fun post(url: String, body: String, headers: Map<String, String> = emptyMap()): HttpResponse
suspend fun put(url: String, body: String, headers: Map<String, String> = emptyMap()): HttpResponse
suspend fun delete(url: String, headers: Map<String, String> = emptyMap()): HttpResponse
}
// Factory function
expect fun createHttpClient(): HttpClient
Extension functions for JSON and forms:
// JSON extensions
suspend fun HttpClient.getJson(url: String): HttpResponse =
get(url, mapOf("Accept" to "application/json"))
suspend fun HttpClient.postJson(url: String, json: String): HttpResponse =
post(url, json, mapOf("Content-Type" to "application/json"))
// Form data extensions
suspend fun HttpClient.postForm(url: String, formData: Map<String, String>): HttpResponse {
val body = formData.entries.joinToString("&") { "${it.key}=${it.value}" }
return post(url, body, mapOf("Content-Type" to "application/x-www-form-urlencoded"))
}
Usage:
@Composable
fun DataComponent() {
val data = remember { mutableStateOf<String?>(null) }
val isLoading = remember { mutableStateOf(false) }
val httpClient = remember { createHttpClient() }
LaunchedEffect(Unit) {
isLoading.value = true
try {
val response = httpClient.getJson("/api/data")
if (response.status == 200) {
data.value = response.body
}
} catch (e: Exception) {
console.error("HTTP request failed: ${e.message}")
} finally {
isLoading.value = false
}
}
}
Cross-platform storage abstraction for local, session, and memory storage.
enum class StorageType {
LOCAL, SESSION, MEMORY
}
// Storage interface
expect class Storage {
fun setItem(key: String, value: String)
fun getItem(key: String): String?
fun removeItem(key: String)
fun clear()
fun getKeys(): Set<String>
fun size(): Int
}
// Factory functions
expect fun createLocalStorage(): Storage
expect fun createSessionStorage(): Storage
expect fun createMemoryStorage(): Storage
// Typed storage wrapper
class TypedStorage<T>(
private val storage: Storage,
private val serializer: (T) -> String,
private val deserializer: (String) -> T?
) {
fun set(key: String, value: T) {
storage.setItem(key, serializer(value))
}
fun get(key: String): T? {
val stringValue = storage.getItem(key) ?: return null
return deserializer(stringValue)
}
fun remove(key: String) = storage.removeItem(key)
fun clear() = storage.clear()
}
Usage:
@Composable
fun UserPreferencesComponent() {
val localStorage = remember { createLocalStorage() }
val theme = remember { mutableStateOf("light") }
// Load theme preference on startup
LaunchedEffect(Unit) {
val savedTheme = localStorage.getItem("theme") ?: "light"
theme.value = savedTheme
}
// Save theme when it changes
LaunchedEffect(theme.value) {
localStorage.setItem("theme", theme.value)
}
Button(
onClick = {
theme.value = if (theme.value == "light") "dark" else "light"
},
label = "Toggle Theme (Current: ${theme.value})"
)
}
// Typed storage example
@Composable
fun TypedStorageExample() {
val userStorage = remember {
TypedStorage(
storage = createLocalStorage(),
serializer = { user: User -> Json.encodeToString(user) },
deserializer = { json -> try { Json.decodeFromString<User>(json) } catch (e: Exception) { null } }
)
}
val currentUser = remember { mutableStateOf<User?>(null) }
LaunchedEffect(Unit) {
currentUser.value = userStorage.get("currentUser")
}
}
The Summon effects system allows you to manage side effects in a platform-independent way, while still providing access to platform-specific capabilities when needed. Effects make it easy to integrate with external systems, manage component lifecycle, and keep your UI code clean and focused on presentation.