jwstegemann / fritz2

Easily build reactive web-apps in Kotlin based on flows and coroutines.
https://www.fritz2.dev
MIT License
627 stars 25 forks source link

Reconsider `RootStore` update behavior #858

Open haukesomm opened 3 months ago

haukesomm commented 3 months ago

Current behavior

Currently, a RootStore's value is only updated (i.e. data flow emits a new value) if the Store receives a new value. If the Store receives a value that is equal to the current value, no value is emitted.

This is due to the fact that a MutableStateFlow is used to keep track of the current value which is explicitly designed to behave this way.

Why this is problematic

Imagine a Lens that is used to strip illegal characters from an input (e.g. characters from a phone number). The expected behavior would be like this:

1) Add invalid characters to an input field with a valid input 2) Invalid characters get stripped by the Lens 3) The store is updated 4) The formatting Lens passes the sanitized value upstream to the input 5) The input field is updated to have the sanitized value

Instead, the following happens:

1) Add invalid characters to an input field with a valid input 2) Invalid characters get stripped by the Lens 3) The store is not updated since the sanitized input is the same as the current value 4) Nothing is passed upstream 5) The input field keeps the illegal input

Example code

val allowedCharacters = "abc".toCharArray().toSet()

val sanitizingLens: Lens<String, String> = lensOf(
    format = { it },
    parse = { it.filter { c -> c in allowedCharacters } }
)

val store = storeOf("").map(sanitizingLens)

div("space-y-4") {
    input {
        type("text")
    }.changes.values() handledBy store.update

    p {
        store.data.renderText(into = this)
    }
}

Proposal

Consider one of the following options:

1) Introduce a flag to explictly update the Store's data on any new input 2) Change the Store's default behavior to always emit a new value, regardless of the equality of the input

Lysander commented 3 months ago

This needs carefully investigation in order to keep the current and long time established behaviour stable. I am in favor of some flag or somehow "store"-overloading / variant / behaviour approach right now. On the other hand besides "legacy" code we want to support users with our framework to write idiomatic code. That would mean that the current behaviour should rather be treated and branded as deprecated, which in dead would mean some API change.

haukesomm commented 3 months ago

In an internal meeting @Lysander suggested to change the default behavior of the RootStore class and filter out identical values on the rendering level instead.

This way the stores would always update but identical contents would not be rendered multiple times.

Lysander commented 1 month ago

Another use case that is currently unnecassary hard to solve with the store behaviour:

Imagine some "trigger", that should start some process on any new impulse. The naive but rational approach would look like this:

val trigger = storeOf(Unit)

// some UI that should reacts to the trigger

// below the trigger element
button {
    clicks handledBy trigger.update // the "new" Unit gets swallowed :-/
}

To overcome the false "distinctUntilChanged" logic of the store, you could rely on the emittingHandler-concept:

val trigger = storeOf(Unit).handleAndEmit<Unit, Unit> { _, _ -> emit(Unit) }

Looks complicated and cumbersome imho!

Lysander commented 1 month ago

Current insights:

We would need to change both code places therefore.