You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Oleksandr Karpovich 656592124a
web: add a tutorial for DomSideEffect (#975)
3 years ago
..
README.md web: add a tutorial for DomSideEffect (#975) 3 years ago

README.md

Using Effects in Compose Web

The API is not finalized, and breaking changes can be expected

Introduction

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, while ref can be used only in attrs 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 a key: Any, the effect will be disposed and reset when key value changes. When it's used without a key, then it behaves like ref - 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)")
        }
    }
}