Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reactive decorator #16

Open
DrSensor opened this issue Sep 10, 2022 · 4 comments
Open

Reactive decorator #16

DrSensor opened this issue Sep 10, 2022 · 4 comments
Labels
data binding Everything related to data binding decorator All issues related to ES decorator enhancement New feature or request priority: low Low hanging issue. Might not worth to tackle it now.

Comments

@DrSensor
Copy link
Owner

DrSensor commented Sep 10, 2022

import { reactive, autorun, unreact } from "https://esm.run/wiles/decorator"
import { clear, halt, listen } from "https://esm.run/wiles/std"
export default class Global {
  static total = 0
  @reactive accessor count: number

  /** imply @derive with count as dependency */
  get half() { return this.count / 2 }

  increment() { this.count++ }

  /** automatically run when instantiated (implicit) */
  @autorun.now #halfSum() { Global.total += this.half }

  /** need to be called to register the effect (explicit) */
  @autorun enableLog() { console.log(this.count) }

  constructor({
    log = true,
  } = {}) {
    if (log) this.enableLog()
  }

  disableLog() { clear(this.enableLog) }

  toggleLog() {
    if (this.enableLog == clear) {
      console.warn("log permanently disabled")
      return
    }
    if (this.enableLog == halt) listen(this.enableLog)
    else if (this.enableLog == listen) halt(this.enableLog)
  }
}

Note no need for @derive decorator (maybe 🤔)

Behaviour:

  1. When method for event handler is called, the autorun happen after event handler is finished. This give a chance to conditionally disable autorun when assigning the reactive accessor. (i.e if (this.count++ < 5) unreact(this, "count"))
  2. Else the autorun will just run immediately when assigning the reactive accessor.

📚 References

@DrSensor DrSensor added enhancement New feature or request priority: low Low hanging issue. Might not worth to tackle it now. data binding Everything related to data binding labels Sep 10, 2022
@DrSensor DrSensor added the decorator All issues related to ES decorator label Oct 29, 2022
@DrSensor
Copy link
Owner Author

DrSensor commented Mar 5, 2023

💡 Wild Idea

NO NEED FOR DECORATOR❗

Just effect(...) and done

export default class {
  count = 0
  tick = 0
  constructor() {
    effect(passive => console.log(
      this.count,   // tracked
      passive.tick, // not tracked
    ))
  }

  // <span :: text:=computed />
  get computed() {
    return effect(passive => passive.tick + this.count)
  }
  /* or can be simplified as
  readonly computed = effect(passive => passive.tick + this.count)
  */

  // <button :: on:click="increment">++</button>
  increment() { this.count++ } // print into console

  // <button :: on:click="decrement">++</button>
  decrement() { this.tick++ } // update <span> text
}

Note that due to automatic binding (loose mode), effect only run after constructor even though it's called inside it. Using effect as decorator might make more sense 🤔

import { effect, passive } from "nusa/reactive" // yup, this module `export let passive`

export default class {
  count = 0
  tick = 0

  constructor() {}

  @effect #logger() {
    console.log(
      this.count,   // tracked
      passive.tick, // not tracked
    ))
  }

  // <span :: text:=computed />
  @effect get computed() { return passive.tick + this.count }

  // <button :: on:click="increment">++</button>
  increment() { this.count++ } // print into console

  // <button :: on:click="decrement">++</button>
  decrement() { this.tick++ } // update <span> text
}

Another approach is by signaling that class scope cause reactive effect

export default class {
  count = 0
  tick = 0

  constructor() {
    this.#enableLogger() // WARNING: need to be explicitly called
  }

  #enableLogger() {
    const passive = effect() // signal that this method may cause reactive effect
    console.log(
      this.count,   // tracked
      passive.tick, // not tracked
    ))
  }

  // <span :: text:=computed />
  get computed() {
    const passive = effect()
    return passive.tick + this.count
  }

  // <button :: on:click="increment">++</button>
  increment() { this.count++ } // print into console

  // <button :: on:click="decrement">++</button>
  decrement() { this.tick++ } // update <span> text
}

🧑‍⚖️ Verdict: decorator WIN!


@DrSensor
Copy link
Owner Author

DrSensor commented Mar 6, 2023

Using it on specific lifecycle 🤔

import { use } as at from "nusa/std"
import { effect, idle } from "nusa/reactive"
import * as at from "nusa/lifecycle"

import Counter from "./counter.js"
import Program from "./program.js" // let say it extends Counter
// and both modules <link> inside the same <render-scope>

at.render(() => {
  const counter = use(Counter), $counter = idle(counter)
  const program = use(Program), $program = idle(program)
  // or maybe `$program = effect.passive(program)` 🤔
  effect(() => {
    console.log("Counter:"
    , counter.count   // tracked
    , $counter.tick)  // not tracked
    console.log("Program:"
    , $program.count  // not tracked
    , program.tick)   // tracked
  })
})

@DrSensor
Copy link
Owner Author

DrSensor commented Mar 20, 2023

Options

reset

default: false (for performance)

Reset dependency graph after effect finish executed. When false, the dependency graph will incrementally build up instead of being renewed each time effect is executed.

export default class {
  location = '37°46′39″N 122°24′59″W'
  zipCode = "94103"
  preference = "location"

  // @effect({ reset: true })
  @effect.reset get weather() {
    switch (this.preference) {
      case "location":
        return lookUpWetherByGeo(this.location)
      case "zip":
        return lookUpWetherByZip(this.zipCode)
      default: 
        return null
    }
  }
}

props/attrs bound to weather will get updated either when:

  • each time this.preference change
  • each time this.location change only when the previous this.preference === "location"
  • each time this.zipCode change only when the previous this.preference === "zip"

@DrSensor
Copy link
Owner Author

DrSensor commented May 15, 2023

Making every properties and accessors reactive when initialize effect decorator is problematic because there is no way to get accessor of private field via Class.prototype. So marking them as @active accessor is necessary.

export default class {
  @active accessor tick = 0
  @active static accessor #sum = 0

  @effect get blow() { // not run until it's accessed
    return (this.#sum -= this.tick)
  }
  @effect grow(value = 0) { // not run until it's called
    if (effect.invoke) { // first time invoke
      this.#sum += value
      this.tick // listen to tick changes
    } else { // continuous effect
      this.#sum += this.tick + value
    }
    // TODO: How to persist last `value` 🤔
  }
  @effect.invoke #log() { // run immediately after instantiation
    console.log("tick", this.tick)
    console.log("sum", this.#sum)
  }
}

So yeah, @effect should be companied with @active for the sake of consistency, no magic. Though there is a pattern where you don't need @active accessor be in the same class as @effect method.

TODO:

  • How to persist arguments in @effect method

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
data binding Everything related to data binding decorator All issues related to ES decorator enhancement New feature or request priority: low Low hanging issue. Might not worth to tackle it now.
Projects
None yet
Development

No branches or pull requests

1 participant