ObservableStore icon indicating copy to clipboard operation
ObservableStore copied to clipboard

Introduce KeyedCursorProtocol, remove ViewStore in favor of forward

Open gordonbrander opened this issue 3 years ago • 0 comments

This PR sketches out one potential solution to #18. It refactors our approach to sub-components by decomplecting action sending from state getting.

  • Removes ViewStore
  • Introduces Address.forward(send:tag:) which gives us an easy way to create tagged send functions. This solves one part of what ViewStore was solving.
  • Introduces Binding(state:send:tag:) which gives us the binding equivalent to Address.forward
  • Introduces KeyedCursorProtocol which offers an alternative cursor for subcomponents that need to be looked up within dynamic lists.

This refactor is in response to the awkwardness of the ViewStore/Cursor paradigm for components that are part of a dynamic list. Even if we had created a keyed cursor initializer for ViewStore, it necessarily would have had to hold an optional (nillable) state. This is because ViewStore lookup was dynamic, and this trips up the lifetime typechecking around the model. In practice, a view would not exist if its model did not exist, but this is not a typesafe guarantee for dynamic list lookups.

Anyway, the whole paradigm of looking up child from parent dynamically is a bit odd for list items. In SwiftUI the typical approach is to ForEach, and then pass the model data down as a static property to the view. This guarantees type safety, since a view holds its own copy of the data. What if we could do something more like that?

The approach in this PR leans into this approach. State can be passed to sub-components as plain old properties. Address.forward can be used to create view-local send functions that you can pass down to sub-views. Binding gets a similar form. In both cases, we can use a closure to capture additional parent-scoped state, such as an ID for lookup within the parent model.

Cursor sticks around, but mostly as a convenient way to create update functions for sub-components. We also introduce KeyedCursorProtocol which offers a keyed equivalent for dynamic lookup.

Usage

Sub-components become more "vanilla", just using bare properties and closures.

struct ParentView: View {
    @StateObject = Store(
        AppModel(),
        AppEnvironment()
    )

    var body: some View {
        ChildModel(
            state: store.state.child,
            send: Address.forward(
                send: store.send,
                tag: ParentChildCursor.tag
            )
        )
    }
}

struct ChildView: View {
    var state: ChildModel
    var send: (ChildAction) -> Void

    var body: some View {
        Button(state.text) {
            send(.activate)
        }
    }
}

Prior art

This approach is inspired by Reflex:

  • Forward https://github.com/mozilla/reflex/blob/c5e75e98bc601e2315b6d43e5e347263cf67359e/src/signal.js#L5
  • Cursor https://github.com/browserhtml/browserhtml/blob/master/src/Common/Cursor.js

gordonbrander avatar Sep 21 '22 16:09 gordonbrander