Skip to content

Commit 29b6dc1

Browse files
committed
docs: add memo, effect, computed
1 parent 95966b4 commit 29b6dc1

4 files changed

Lines changed: 309 additions & 121 deletions

File tree

docs/jsx.md

Lines changed: 165 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Can be written as
4141
```tsx
4242
function Box() {
4343
const [counter, setCounter] = createState(0)
44-
const label = createComputed((get) => `clicked ${get(counter)} times`)
44+
const label = createComputed(() => `clicked ${counter()} times`)
4545

4646
function onClicked() {
4747
setCounter((c) => c + 1)
@@ -200,8 +200,8 @@ function MyWidget() {
200200
### Bindings
201201

202202
Properties can be set as a static value. Alternatively, they can be passed an
203-
[Accessor](./jsx#accessor), in which case whenever its value changes, it will be
204-
reflected on the widget.
203+
[Accessor](#state-management), in which case whenever its value changes, it will
204+
be reflected on the widget.
205205

206206
```tsx
207207
const [revealed, setRevealed] = createState(false)
@@ -366,6 +366,31 @@ function MyWidget({ label, onClicked }: MyWidgetProps) {
366366
}
367367
```
368368

369+
> [!TIP]
370+
>
371+
> To make reusable function components more convenient to use, you should
372+
> annotate props as either static or dynamic and handle both cases as if it was
373+
> dynamic.
374+
>
375+
> ```ts
376+
> type $<T> = T | Accessor<T>
377+
> const $ = <T>(value: $<T>): Accessor<T> =>
378+
> value instanceof Accessor ? value : new Accessor(() => value)
379+
> ```
380+
381+
```tsx
382+
function Counter(props: {
383+
count?: $<number>
384+
label?: $<string>
385+
onClicked?: () => void
386+
}) {
387+
const count = $(props.count)((v) => v ?? 0)
388+
const label = $(props.label)((v) => v ?? `Fallback label ${count()}`)
389+
390+
return <Gtk.Button label={label} onClicked={props.onClicked} />
391+
}
392+
```
393+
369394
## Control flow
370395
371396
### Dynamic rendering
@@ -385,8 +410,7 @@ return (
385410
> [!TIP]
386411
>
387412
> In a lot of cases, it is better to always render the component and set its
388-
> `visible` property instead. This is because `<With>` will destroy/recreate the
389-
> widget each time the passed `value` changes.
413+
> `visible` property instead.
390414
391415
> [!WARNING]
392416
>
@@ -434,16 +458,30 @@ removing.
434458
435459
## State management
436460
437-
There is a single primitive called `Accessor`, which is a read-only signal.
461+
There is a single primitive called `Accessor`, which is a read-only reactive
462+
value. It is the base of Gnim's reactive system. They are essentially functions
463+
that let you read a value and track it in reactive scopes so that when they
464+
change the reader is notified.
438465
439466
```ts
440-
export interface Accessor<T> {
441-
get(): T
442-
subscribe(callback: () => void): () => void
443-
<R = T>(transform: (value: T) => R): Accessor<R>
467+
interface Accessor<T> {
468+
(): T
469+
peek(): T
470+
subscribe(callback: Callback): DisposeFn
444471
}
472+
```
473+
474+
There are two ways to read the current value:
475+
476+
- `(): T`: which returns the current value and tracks it as a dependency in
477+
reactive scopes
478+
- `peek(): T` which returns the current value **without** tracking it as a
479+
dependency
445480
446-
let accessor: Accessor<any>
481+
To subscribe for value changes you can use the `subscribe` method.
482+
483+
```ts
484+
const accessor: Accessor<any>
447485
448486
const unsubscribe = accessor.subscribe(() => {
449487
console.log("value of accessor changed to", accessor.get())
@@ -452,9 +490,14 @@ const unsubscribe = accessor.subscribe(() => {
452490
unsubscribe()
453491
```
454492
493+
> [!WARNING]
494+
>
495+
> The subscribe method is not scope aware. Do not forget to clean them up when
496+
> no longer needed. Alternatively, use an [`effect`](#createeffect) instead.
497+
455498
### `createState`
456499
457-
Creates a writable signal.
500+
Creates a writable reactive value.
458501
459502
```ts
460503
function createState<T>(init: T): [Accessor<T>, Setter<T>]
@@ -470,51 +513,60 @@ setValue(2)
470513
setValue((prev) => prev + 1)
471514
```
472515
473-
### `createComputed`
516+
> [!IMPORTANT]
517+
>
518+
> Effects and computations are only triggered when the value changes.
474519
475-
Creates a computed signal from a producer function that tracks its dependencies.
520+
By default, equality between the previous and new value is checked with
521+
[Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
522+
and so this would not trigger an update:
476523
477524
```ts
478-
export function createComputed<T>(
479-
producer: (track: <V>(signal: Accessor<V>) => V) => T,
480-
): Accessor<T>
525+
const [object, setObject] = createState({})
526+
527+
// this does NOT trigger an update by default
528+
setObject((obj) => {
529+
obj.field = "mutated"
530+
return obj
531+
})
481532
```
482533
483-
Example:
534+
You can pass in a custom `equals` function to customize this behavior:
484535
485536
```ts
486-
let a: Accessor<number>
487-
let b: Accessor<number>
488-
489-
const c = createComputed((get) => get(a) + get(b))
537+
const [value, setValue] = createState("initial value", {
538+
equals: (prev, next): boolean => {
539+
return prev != next
540+
},
541+
})
490542
```
491543
492-
Alternatively, you can specify a list of dependencies, in which case values are
493-
passed to an optional transform function.
544+
### `createComputed`
545+
546+
Create a computed value which tracks dependencies and invalidates the value
547+
whenever they change. The result is cached and is only computed on access.
494548
495549
```ts
496-
function createComputed<
497-
Deps extends Array<Accessor<any>>,
498-
Values extends { [K in keyof Deps]: Accessed<Deps[K]> },
499-
>(deps: Deps, transform: (...values: Values) => V): Accessor<V>
550+
function createComputed<T>(compute: () => T): Accessor<T>
500551
```
501552
502553
Example:
503554
504555
```ts
505-
let a: Accessor<string>
506-
let b: Accessor<string>
556+
let a: Accessor<number>
557+
let b: Accessor<number>
507558
508-
const c = createComputed([a, b], (a, b) => `${a}+${b}`)
559+
const c: Accessor<number> = createComputed(() => a() + b())
509560
```
510561
511562
> [!TIP]
512563
>
513-
> There is a shorthand for single dependency computed signals.
564+
> There is a shorthand for computed values.
514565
>
515566
> ```ts
516567
> let a: Accessor<string>
517-
> const b: Accessor<string> = a((v) => `transformed ${v}`)
568+
> const b = createComputed(() => `transformed ${a()}`)
569+
> const b = a((v) => `transformed ${v}`) // alias for the above line
518570
> ```
519571
520572
### `createBinding`
@@ -535,13 +587,57 @@ const styleManager = Adw.StyleManager.get_default()
535587
const style = createBinding(styleManager, "colorScheme")
536588
```
537589
590+
It also supports nested bindings.
591+
592+
```ts
593+
interface Outer extends GObject.Object {
594+
nested: Inner | null
595+
}
596+
597+
interface Inner extends GObject.Object {
598+
field: string
599+
}
600+
601+
const value: Accessor<string | null> = createBinding(outer, "nested", "field")
602+
```
603+
604+
### `createEffect`
605+
606+
Schedule a function to run after the current Scope created with
607+
[`createRoot`](#createroot) returns, tracking dependencies and re-running the
608+
function whenever they change.
609+
610+
```ts
611+
function createEffect(fn: () => void): void
612+
```
613+
614+
Example:
615+
616+
```ts
617+
const count: Accessor<number>
618+
619+
createEffect(() => {
620+
console.log(count()) // reruns whenever count changes
621+
})
622+
623+
createEffect(() => {
624+
console.log(count.peek()) // only runs once
625+
})
626+
```
627+
628+
> [!CAUTION]
629+
>
630+
> Effects are a common pitfall for beginners to understand when to use and when
631+
> not to use them. You can read about
632+
> [when it is discouraged and their alternatives](./tutorial/gnim.md#when-not-to-use-an-effect).
633+
538634
### `createConnection`
539635
540636
```ts
541637
function createConnection<
542638
T,
543639
O extends GObject.Object,
544-
S extends keyof O1["$signals"],
640+
S extends keyof O["$signals"],
545641
>(
546642
init: T,
547643
handler: [
@@ -562,7 +658,7 @@ arguments passed by the signal and the current value as the last parameter.
562658
Example:
563659
564660
```ts
565-
const value = createConnection(
661+
const value: Accessor<string> = createConnection(
566662
"initial value",
567663
[obj1, "notify", (pspec, currentValue) => currentValue + pspec.name],
568664
[obj2, "sig-name", (sigArg1, sigArg2, currentValue) => "str"],
@@ -574,6 +670,36 @@ const value = createConnection(
574670
> The connection will only get attached when the first subscriber appears, and
575671
> is dropped when the last one disappears.
576672
673+
### `createMemo`
674+
675+
Create a derived reactive value which tracks its dependencies and re-runs the
676+
computation whenever a dependency changes. The resulting `Accessor` will only
677+
notify subscribers when the computed value has changed.
678+
679+
```ts
680+
function createMemo<T>(compute: () => T): Accessor<T>
681+
```
682+
683+
It is useful to memoize values that are dependencies of expensive computations.
684+
685+
Example:
686+
687+
```ts
688+
const value = createBinding(gobject, "field")
689+
690+
createEffect(() => {
691+
console.log("effect1", value())
692+
})
693+
694+
const memoValue = createMemo(() => value())
695+
696+
createEffect(() => {
697+
console.log("effect2", memoValue())
698+
})
699+
700+
value.notify("field") // triggers effect1 but not effect2
701+
```
702+
577703
### `createSettings`
578704
579705
Wraps a `Gio.Settings` into a collection of setters and accessors.
@@ -627,15 +753,15 @@ const counter = createExternal(0, (set) => {
627753
628754
## Scopes and Life cycle
629755
630-
A scope is essentially a global object which holds cleanup functions and context
631-
values.
756+
A [scope](./tutorial/gnim.md#scopes) is essentially a global object which holds
757+
cleanup functions and context values.
632758
633759
```js
634760
let scope = new Scope()
635761
636762
// Inside this function, synchronously executed code will have access
637-
// to `scope` and will attach any allocated resource, such as signal
638-
// subscriptions, to the `scope`.
763+
// to `scope` and will attach any allocated resources, such as signal
764+
// subscriptions.
639765
scopedFuntion()
640766
641767
// At a later point it can be disposed.

0 commit comments

Comments
 (0)