6.6 KiB
Using Effects in Compose Web
Compose for Web introduces a few dom-specific effects on top of existing effects from Compose.
ref in AttrsBuilder
Under the hood, ref
uses DisposableEffect
ref
can be used to retrieve a reference to a html element.
The lambda that ref
takes in is not Composable. It will be called only once when an element added into a composition.
Likewise, the lambda passed in onDispose
will be called only once when an element leaves the composition.
Div(attrs = {
ref { htmlDivElement ->
// htmlDivElement is a reference to the HTMLDivElement
onDispose {
// add clean up code here
}
}
}) {
// Content()
}
Only one ref
can be used per element. Calling it more than once will dismiss earlier calls.
For example, ref
can be used to add and remove some event listeners not provided by compose-web from the box.
DisposableRefEffect
Under the hood, DisposableRefEffect
uses DisposableEffect
DisposableRefEffect
is similar to ref
, since it also provides a reference to an element. At the same time it has few differences.
DisposableRefEffect
can be added only within a content lambda of an element, whileref
can be used only inattrs
scope.- Unlike
ref
,DisposableRefEffect
can be used as many times as needed and every effect will be unique. - DisposableRefEffect can be used with a
key
and without it. When it's used with akey: Any
, the effect will be disposed and reset whenkey
value changes. When it's used without a key, then it behaves likeref
- the effect gets called only once when an element enters the composition, and it's disposed only when the element leaves the composition.
Div {
// without a key
DisposableRefEffect { htmlDivElement ->
// htmlDivElement is a reference to the HTMLDivElement
onDispose {
// add clean up code here
}
}
}
var state by remember { mutableStateOf(1) }
Div {
// with a key.
// The effect will be called for every new state's value
DisposableRefEffect(state) { htmlDivElement ->
// htmlDivElement is a reference to the HTMLDivElement
onDispose {
// add clean up code here
}
}
}
DomSideEffect
Under the hood, DomSideEffect
uses SideEffect
DomSideEffect
as well as DisposableRefEffect
can be used with a key and without it.
Unlike DisposableRefEffect
, DomSideEffect
without a key is invoked on every successful recomposition.
With a key
, it will be invoked only when the key
value changes.
Same as SideEffect, DomSideEffect
can be helpful when there is a need to update objects not managed by Compose.
In case of web, it often involves updating HTML nodes, therefore DomSideEffect
provides a reference to an element in the lambda.
Code Sample using effects
The code below showcases how it's possible to use non-composable components in Compose by applying DomSideEffect
and DisposableRefEffect
.
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.Composable
import kotlinx.browser.document
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLParagraphElement
// Here we pretend that `RedBoldTextNotComposableRenderer`
// wraps a UI logic provided by 3rd party library that doesn't use Compose
object RedBoldTextNotComposableRenderer {
fun unmountFrom(root: HTMLElement) {
root.removeChild(root.firstChild!!)
}
fun mountIn(root: HTMLElement) {
val pElement = document.createElement("p") as HTMLParagraphElement
pElement.setAttribute("style", "color: red; font-weight: bold;")
root.appendChild(pElement)
}
fun renderIn(root: HTMLElement, text: String) {
(root.firstChild as HTMLParagraphElement).innerText = text
}
}
// Here we define a Composable wrapper for the above code. Here we use DomSideEffect and DisposableRefEffect.
@Composable // @param `show: Boolean` was left here intentionally for the sake of the example
fun ComposableWrapperForRedBoldTextFrom3rdPartyLib(state: Int, show: Boolean) {
Div(attrs = {
style {
backgroundColor(Color.lightgray)
width(100.px)
minHeight(40.px)
padding(30.px)
}
}) {
if (!show) {
Text("No content rendered by the 3rd party library")
}
Div {
if (show) {
// Update the content rendered by "non-compose library" according to the `state`
DomSideEffect(state) { div ->
RedBoldTextNotComposableRenderer.renderIn(div, "Value = $state")
}
}
DisposableRefEffect(show) { div ->
if (show) {
// Let "non-compose library" control the part of the page.
// The content of this div is independent of Compose.
// It will be managed by RedBoldTextNotComposableRenderer
RedBoldTextNotComposableRenderer.mountIn(div)
}
onDispose {
if (show) {
// Clean up the html created/managed by "non-compose library"
RedBoldTextNotComposableRenderer.unmountFrom(div)
}
}
}
}
}
}
fun main() {
var state by mutableStateOf(0)
var showUncontrolledElements by mutableStateOf(false)
renderComposable(rootElementId = "root") {
ComposableWrapperForRedBoldTextFrom3rdPartyLib(state = state, show = showUncontrolledElements)
Div {
Label(forId = "checkbox") {
Text("Show/hide text rendered by 3rd party library")
}
CheckboxInput(checked = false) {
id("checkbox")
onInput {
showUncontrolledElements = it.value
}
}
}
Button(attrs = {
onClick { state += 1 }
}) {
Text("Incr. count ($state)")
}
}
}