Skip to content

Conversation

@Aylur
Copy link
Owner

@Aylur Aylur commented Nov 23, 2025

This PR introduces two new primitives: createEffect and createMemo. It also overhauls how createComputed and Accessor tracking works. It also adds support for nested properties using createBinding.

Changes

Accessor dependency tracking

Accessor tracking is now builtin to the Accessor implementation and does not require other code to manage it so createComputed syntax is replaced with createComputed(() => dep1() + dep2()).

Note that calling an Accessor as a function previously required passing a transform function which returns a new Accessor. This behavior is not changed, but now its possible to omit it, which simply returns the value and tracks it in reactive scopes as a dependency.

const accessor: Accessor<any>
createComputed(() => {
  return accessor() // tracked as a dependency
  return accessor.peek() // not tracked
})

State custom equals

createState can now take in a custom equals function to customize when updates trigger.
With the changes to createComputed dependencies are no longer aggressively cached.

const field = createBinding(gobject, "field")

const v = createComputed(() => field())

gobject.notify("field") // used to not trigger `v` to re-run, now it does

the previous behavior can still be achieved with a memo

Memo

New primitive: createMemo.

Its signature is identical to createComputed but instead of only invalidating and only computing when accessed createMemo always re-runs the computation but will not trigger effect/computation updates that depend on it

const field = createBinding(gobject, "field")
const memoizedField = createMemo(() => field())
const v = createComputed(() => memoizedField())

createEffect(() => console.log(field()))
createEffect(() => console.log(memoizedField()))

gobject.notify("field") // will trigger the memo fn and first effect, but not the computed fn and second effect

Effects

New primitive: createEffect

Instead of avoiding it, the docs simply warn users when not to use it. It probably won't stop users to write bad code but at least resources are automatically managed for them.

const accessor: Accessor<any>
createEffect(() => {
  console.log("value changed", accessor()) // whenever value changes, effect reruns
  console.log(accessor.peek()) // peek() can be used to read without tracking it as a dependency
})

Nested bindings

createBinding now finally supports nested properties

interface Outer extends GObject.Object {
  nestedNullable: Inner | null
  nested: Inner
}

interface Inner extends GObject.Object {
  field: string
}

const a: Accessor<string | null> = createBinding(outer, "nestedNullable", "field")
const b: Accessor<string> = createBinding(outer, "nested", "field")

TypeScript annotations only support up to 4 levels for now which should be plenty enough for any usecase, but the runtime supports an unlimited number of deepness so if 4 levels is not enough you can slap a @ts-expect-error on it.

Deprecations

createComputed syntax

With this PR the previous two createComputed syntax is deprecated.

  • createComputed([dep1, dep2], (v1, v2) => v1 + v2) and
  • createComputed((get) => get(dep1) + get(dep2))

Accessor.get

To reflect its usecase better it has been renamed to .peek()

@Aylur Aylur merged commit c821e8e into main Nov 27, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants