Skip to content

Alternative idea to simplify reactive code #223

Closed
@jods4

Description

@jods4

I don't want to spam #222 with more comments unrelated to the RFC design than I already did.
If some admin wants to move the comments related to auto here, please do.

Goal

Enable users to write JS code that is reactive, while remaining as "plain" as possible.
In other words: use toolchain to remove the need to manipulate refs and write .value.

This is an alternative idea to ref: which was introduced in #222.
As we shall see it is not an alternative to <script setup>, as it is totally orthogonal to that proposal.

Example

Here's what the traditional Todo List would look like using this idea and the export idea from <script setup>:

<script setup>
  import { auto, reactive } from "vue"

  let todos = reactive([])

  // checkbox showing/hiding completed todos
  export let showCompleted = auto.ref(false) 
  // filtered list of todos
  export let shownTodos = auto.computed(() => todos.filter(t => showCompleted || !t.completed))
  // count of todos displayed
  export let count = auto.computed(() => shownTodos.length)

  // textbox for new todo
  export let newTodo = auto.ref("") 
  // button to add said todo
  export function add() {
    if (newTodo == "") return
    todos.push({ name: newTodo, completed: false })
    newTodo = ""
  }
</script>

Observe that outside of variable initializations, which uses auto, it's plain JS code. You can read if (newTodo == "") and write newTodo = "" reactive variables normally, in fact newTodo is typed as plain string in this code.

If you're familiar with Vue 3 reactivity, also observe that it's the same initialization, except we've swapped auto.ref for ref and auto.computed for computed.

API

This introduce a single new function called auto:

function auto<T>(value: Ref<T>): T

It takes a ref and unwraps its value, except the result is still reactive.

Because auto(ref(x)) and auto(computed(() => y)) would be very common, two shortcuts functions are provided that perform just that: auto.ref and auto.computed.

Sometimes you need to access a ref directly, not its value. For example if you want to pass it to a function that expects reactive inputs. For this purpose, ref() is re-purposed and will return the initial value that was passed to auto().

// Example function that takes Ref as input
function useLength(text: Ref<string>) {
  return computed(() => unref(text).length)
}

// How it can be called with auto refs:
let x = auto.ref("abcd")
let length = useLength(ref(x))

// Alternatively, you can keep the ref in another variable if you prefer:
let xRef = ref("abcd")
let x = auto(xRef)
let length = useLength(xRef)

Also, if you write advanced code that manipulates refs in weird ways, you can mix regular refs code with auto refs code to your heart's content, it's just javascript after all.

Implementation

Plain JS variables can't be reactive.
This all works because the code is instrumented by a JS transform that would be provided by a loader, e.g. vue-auto-loader. In fact, this can all be done without core Vue support.

That JS loader will track variables that are initialized with auto calls.

  1. The initializer auto() call is removed. let x = auto.ref(3) becomes let x = ref(3) and x is actually the ref at runtime.
  2. Any read or write from such a variable x is replaced by a read or write to x.value (except pattern 3 below).
  3. Calls to ref(x) where x is a tracker auto-ref are removed and replaced by x.

Those changes are visible when debugging code without source maps. It's pretty trivial transformations and the result is idiomatic Vue code.

Using auto() in any place other than a variable initializer is an error that would be reported by the loader.

Tooling

The original code is valid Javascript, with the correct and expected semantics.

This means that you can edit this in any IDE: Atom, Sublime, VS Code, etc, without any plugin and still get complete language support (completion, refactorings, code navigation, etc).

You can use any language: Javascript, Typescript or even Coffeescript if you like, as the loader operates on the resulting JS code.

This works in any place, including .vue SFC but also regular standalone JS/TS files, which is handy if you write composable functions or template-less components.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions