ElianHugh / rsx

Encapsulated Shiny modules
https://elianhugh.github.io/rsx/
GNU General Public License v3.0
2 stars 0 forks source link

data sharing across components #12

Open ElianHugh opened 1 year ago

ElianHugh commented 1 year ago

component data is intentionally isolated from other components so as to encourage comprehensible component hierarchies. however, there are situations where accessing component data externally (and non-hierarchically) is a requirement.

Possible solutions

Hierarchical reactive data

One method of hierarchical and reactive data sharing is via defining reactive values on a top-level component and passing those along to subsequent child components.

E.g.

y <- component() {
    data = function() {
        list(
            a = NULL
        )
    }
}

x <- component(
    data = function() {
        list(
            rctv = shiny::reactiveVal()
        )
    }, template = function(ns) {
        y(data = list(a = self$rctv))
    }
)

Barring the eventuality of a long chain of data passing, this has issues with understanding how data may mutate

Event bus

Like how vue does it -- basically just an event emitter

x <- event_bus()
x$emit()

Isolated reactives

Wrapper class around shiny::reactiveVal and/or shiny::reactiveValues that isolates value access from the children of components with data. E.g.

x <- component(
    data = function() {
        list(
            rctv = rsx::reactive_value()
        )
    }, template = function(ns) {
        y(data = list(a = self$rctv))
    }
)

y <- component() {
    data = function() {
        list(
            a = NULL
        )
    }, methods = list(
        setup = function(input, output, session) {
            print(self$a)              # legal usage
            self$a <- self$a + 1L # illegal usage
        }
    )
}

stateful object

Object that contains reactive values that can be accessed in any component, similar to vuex

my_store <- shiny::reactiveValues(
    a = 1,
    b = 2,
    c = 3
)

x <- component(
    data = function() {
        a = my_store$a,
        b = my_store$b,
        c = my_store$c
    }
)

callback (child to parent)

x <- component(
    data = function() {
        list(
            a = 1L,
            update_a = function() {
                self$a <- self$a + 1L
            }
        )
    },
    template = function(ns) {
        y(data = list(cb = update_a))
    }
)

y <- component() {
    data = function() {
        list(
            cb = NULL
        )
    }, methods = list(
        setup = function(input, output, session) {
            self$cb()
        }
    )
}
ElianHugh commented 1 year ago

Leaning toward the idea of isolated reactives, but this would ideally need a nice interface. Some ideas:

Wrapper

data = function() {
  list(
        x = reactive_value()
  )
}

Issues with this approach:

New component property

props = function() {
  list()
}

This creates a nice flow of props to data when passing values. E.g.

x <- component(
    state = function() {
        list(
            rctv = 1L
        )
    }, template = function(ns) {
        y(data = list(a = self$rctv))
    }
)

y <- component() {
    data = function() {
        list(
            a = NULL
        )
    }
}

Also ensures that the state cannot be modified outside the component (because only data can be passed between components)

Issues:

ElianHugh commented 1 year ago

New component property: state

Steps to implementation:

  1. Add state arguments to component generator functions https://github.com/ElianHugh/rsx/blob/68cca166ae158cce69ebebab378e5075c2167766/R/component.R#L15 https://github.com/ElianHugh/rsx/blob/68cca166ae158cce69ebebab378e5075c2167766/R/component.R#L20-L27 https://github.com/ElianHugh/rsx/blob/68cca166ae158cce69ebebab378e5075c2167766/R/component.R#L35

  2. Add state attribute to component generator function

https://github.com/ElianHugh/rsx/blob/68cca166ae158cce69ebebab378e5075c2167766/R/component.R#L37-L49

  1. Add state generator validator (check is function)

https://github.com/ElianHugh/rsx/blob/68cca166ae158cce69ebebab378e5075c2167766/R/component.R#L52

  1. Create wrapper class for state values (may have to re-implement shiny accessor methods)

  2. Generate state values inside instance via instantiator function https://github.com/ElianHugh/rsx/blob/68cca166ae158cce69ebebab378e5075c2167766/R/instance.R#L34-L41

  3. Add state values (reactiveVals) to instance self environment https://github.com/ElianHugh/rsx/blob/68cca166ae158cce69ebebab378e5075c2167766/R/instance.R#L50

  4. Create active binding "state" alongside "data" and "method" bindings in instance https://github.com/ElianHugh/rsx/blob/68cca166ae158cce69ebebab378e5075c2167766/R/instance.R#L72-L91

  5. Add testthat tests for state