From d2978b5c9938d0ec6881cc1b5f142def677425c4 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Tue, 17 Dec 2024 20:19:07 -0500 Subject: [PATCH] `createBatch` reference doc rewrite (#982) --- .../basic-reactivity/create-effect.mdx | 1 + .../reference/reactive-utilities/batch.mdx | 90 ++++++++++++++++++- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/routes/reference/basic-reactivity/create-effect.mdx b/src/routes/reference/basic-reactivity/create-effect.mdx index 9f8cb1c51..d93501967 100644 --- a/src/routes/reference/basic-reactivity/create-effect.mdx +++ b/src/routes/reference/basic-reactivity/create-effect.mdx @@ -43,6 +43,7 @@ createEffect((prevSum) => { ``` Effects are meant primarily for side effects that read but don't write to the reactive system: it's best to avoid setting signals in effects, which without care can cause additional rendering or even infinite effect loops. Instead, prefer using [createMemo](/reference/basic-reactivity/create-memo) to compute new values that depend on other reactive values, so the reactive system knows what depends on what, and can optimize accordingly. +If you do end up setting a signal within an effect, computations subscribed to that signal will be executed only once the effect completes; see [`batch`](/reference/reactive-utilities/batch) for more detail. The first execution of the effect function is not immediate; it's scheduled to run after the current rendering phase (e.g., after calling the function passed to [render](/reference/rendering/render), [createRoot](/reference/reactive-utilities/create-root), or [runWithOwner](/reference/reactive-utilities/run-with-owner)). If you want to wait for the first execution to occur, use [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) (which runs before the browser renders the DOM) or `await Promise.resolve()` or `setTimeout(..., 0)` (which runs after browser rendering). diff --git a/src/routes/reference/reactive-utilities/batch.mdx b/src/routes/reference/reactive-utilities/batch.mdx index c13b753a8..539109ecc 100644 --- a/src/routes/reference/reactive-utilities/batch.mdx +++ b/src/routes/reference/reactive-utilities/batch.mdx @@ -8,7 +8,89 @@ import { batch } from "solid-js" function batch(fn: () => T): T ``` -This is a low level API that is used by Solid to batch updates. -It holds executing downstream computations within the block until the end to prevent unnecessary recalculation. -Solid Store's set method, Mutable Store's array methods, and Effects automatically wrap their code in a batch. -This is useful for when you want to batch a set of computations that are not wrapped in a batch already. +`batch` is a low-level API that batches updates together. +More precisely, `batch(fn)` holds the execution of downstream computations +during the `fn` block, executing them all together once the block `fn` returns. +Thus, instead of a downstream computation executing after every dependency +update, it will update just once at the end of the batch. + +Batching improves performance by avoiding unnecessary recalculation. +Suppose you have a downstream memo `down` that depends on +multiple upstream signals `up1`, `up2`, and `up3`: + +```ts +import { createSignal, createMemo, createEffect } from "solid-js" +const [up1, setUp1] = createSignal(1) +const [up2, setUp2] = createSignal(2) +const [up3, setUp3] = createSignal(3) +const down = createMemo(() => up1() + up2() + up3()) +// For illustration, monitor when `down` gets recomputed: +createEffect(() => console.log(down())) // outputs 6 +``` + +If you directly update all of the upstream signals outside of batch mode, +then `down` will recompute every time. + +```ts +setUp1(4) // recomputes down, outputs 9 +setUp2(5) // recomputes down, outputs 12 +setUp3(6) // recomputes down, outputs 15 +``` + +If instead you update the upstream signals within a `batch`, then `down` +will update only once at the end: + +```ts +batch(() => { + setUp1(10) // doesn't update down yet + setUp2(10) // doesn't update down yet + setUp3(10) // doesn't update down yet +}) // recomputes down, outputs 30 +``` + +The impact is even more dramatic if you have *m* downstream computations +(memos, effects, etc.) that each depend on *n* upstream signals. +Without batching, modifying all *n* upstream signals +would cause *m n* updates to the downstream computations. +With batching, modifying all *n* upstream signals +would cause *m* updates to the downstream computations. +Given that each update takes at least *n* time +(just to read the upstream signals), this cost savings can be significant. +Batching is also especially helpful when the downstream effects include +DOM updates, which can be expensive. + +Solid uses `batch` internally to automatically batch updates for you +in a few cases: + +* Within [`createEffect`](/reference/basic-reactivity/create-effect) + and [`onMount`](/reference/lifecycle/on-mount) + (unless they are outside a [root](/reference/reactive-utilities/create-root)) +* Within the [setter of a store](/reference/store-utilities/create-store#setter) + (which can update several properties at once) +* Within array methods (e.g. `Array.prototype.splice`) of a + [mutable store](/reference/store-utilities/create-mutable) + (which can update several elements at once) + +These save you from having to use `batch` yourself in many cases. +For the most part, automatic batching should be transparent to you, +because accessing a signal or memo will cause it to update if it is out of date +(as of Solid 1.4). For example: + +```ts +batch(() => { + setUp1(11) // doesn't update down yet + setUp2(11) // doesn't update down yet + setUp3(11) // doesn't update down yet + console.log(down()) // recomputes down, outputs 33 + setUp1(12) // doesn't update down yet + setUp2(12) // doesn't update down yet + setUp3(12) // doesn't update down yet +}) // recomputes down, outputs 36 +``` + +You can think of `batch(fn)` as setting a global "batch mode" variable, +calling the function `fn`, and then restoring the global variable to its +previous value. +This means that you can nest `batch` calls, and they will form one big batch. +It also means that, if `fn` is asynchronous, +only the updates before the first `await` will be batched.