Subscribe on changes!

Allow for custom reactive behavior

avatar
Jul 24th 2021

What problem does this feature solve?

Context

Trying to create property validations for my objects through their accessors,I stumbled up the following problem. Throwing an error from a setter method would simply break reactivity.More specifically in the set method when my objects setter is invoked.Ideally I would like to catch the error without breaking the reactivity chain.While my use case may be too specific ,allowing for a customized behavior for reactive object like the already existing CustomRef would make the api more extensive and probably cover a variety of other use cases

What does the proposed API look like?

Custom reactive

I propose either one of the following

Exposing a custom reactive method

export type CustomReactiveFactory = (
  track: (target: object, track: TrackOpTypes, key: unknown) => void,
  trigger: (
    target: object,
    type: TriggerOpTypes,
    key?: unknown,
    newValue?: unknown,
    oldValue?: unknown,
    oldTarget?: Map<unknown, unknown> | Set<unknown>
  ) => void
) => {
  get: (target: Object, key: string | number | symbol, receiver: object) => any
  set: (
    target: Object,
    key: string | number | symbol,
    value: unknown,
    receiver: object
  ) => boolean
}

export function customReactive<T extends object>(
  target: T,
  factory: CustomReactiveFactory
): T

Re export methods track/trigger through the user-facing renderer

Another solution would be re-exporting the method trigger/track from the reactivity package through the vue-runtime dom as implied here @vue-reactivity

avatar
Jul 24th 2021

doesn't a watch with onTrigger work for your specific use case?

avatar
Jul 26th 2021

I would also say a watcher is more adapted for validation. In any case, this needs to go through an RFC. If you still think this is worth pursuing, follw the RFC proccess in the rfcs repo

avatar
Dec 30th 2021

I second the request of @mVIII, to have a customReactive analog to customRef API. But as you said @posva, I guess going through an RFC is the right way. Since I already wrote my issue (over the last 2 hours until I found this issue... 🙈), I'll just post it here instead:

What problem does this feature solve?

For ref's we have customRef, which allows to delay triggering effects. For reactive objects there is no such API, namely customReactive.

In other words, it's currently not possible (that I know of) to delay triggering effects on changes in a reactive object.

The only workaround so far would be building/using a custom Proxy implementation on values assigned to a customRef, to get control about effect tracking and triggering. Which would basically be an exact copy of Vue's Proxy system, just with additional control (and most probably a waste of time 😉).

My end goal

I'd like to build a transactional higher-level API on top of Vue's native reactivity system: Batching a bunch of async changes without running any effects until the end of the transaction. Other reactivity API's have a concept of transactions, namely async 'actions' (Vuex, Redux, MobX, ...). No effect will be run until the outer-most action has finished, guaranteeing that intermediate or incomplete values produced during an action are not visible to the rest of the application until the action has completed.

But without a customReactive, building such a higher-level reactivity API won't be possible.

What does the proposed API look like?

Creates a customized reactive object with explicit control over its dependency tracking and updates triggering. It expects a factory function, which receives track and trigger functions as arguments and should return an object with get and set.

function useDebouncedReactive(value, delay = 200) {
  let timeout
  return customReactive((track, trigger) => {
    return {
      get(target, type, key) {
        track(target, type, key)
        if (type === 'has') return key in target
        if (type === 'iterate') return Object.getOwnPropertyNames(target[key])
        return target[key]
      },
      set(target, type, key, newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          if (type === 'set' || type === 'add') {
            target.key = newValue
          } else if (type === 'delete') {
            delete target.key
          }
          trigger(target, type, key)
        }, delay)
      },
    }
  })
}

export default {
  setup() {
    return {
      user: useDebouncedReactive({ name: 'John', age: 42 })
    }
  }
}

When dealing with Map or Set inside the reactive value, then the example would look more complex of course (also handling set type === 'clear'). But sticking to simple objects and arrays will most probably cover 95% of all use cases.