From cef890c036bfd7440839a892b349ddaddb75cab9 Mon Sep 17 00:00:00 2001 From: David Godinez Date: Wed, 22 Jan 2025 06:32:45 -0700 Subject: [PATCH 01/10] docs: queueing a series of updates --- plugins/ui/docs/add-interactivity/queueing-updates.md | 7 +++++++ plugins/ui/docs/sidebar.json | 4 ++++ 2 files changed, 11 insertions(+) create mode 100644 plugins/ui/docs/add-interactivity/queueing-updates.md diff --git a/plugins/ui/docs/add-interactivity/queueing-updates.md b/plugins/ui/docs/add-interactivity/queueing-updates.md new file mode 100644 index 000000000..029ed367c --- /dev/null +++ b/plugins/ui/docs/add-interactivity/queueing-updates.md @@ -0,0 +1,7 @@ +# Queueing a Series of State Updates + +When you set a state variable, it queues another render. However, there are times when you may need to perform multiple operations on the value before triggering the next render. To achieve this, it's important to understand how `deephaven.ui` batches state updates. + +## `deephaven.ui` batches state updates + +TODO Arman's question!!!! diff --git a/plugins/ui/docs/sidebar.json b/plugins/ui/docs/sidebar.json index 05347b52f..4455ee5c1 100644 --- a/plugins/ui/docs/sidebar.json +++ b/plugins/ui/docs/sidebar.json @@ -74,6 +74,10 @@ { "label": "Respond to Events", "path": "add-interactivity/respond-to-events.md" + }, + { + "label": "Queueing a Series of State Updates", + "path": "add-interactivity/queueing-updates.md" } ] }, From 5ed291ff957a1654072c6475479ba4c75659fa47 Mon Sep 17 00:00:00 2001 From: David Godinez Date: Wed, 22 Jan 2025 07:02:45 -0700 Subject: [PATCH 02/10] updates use_state.md --- plugins/ui/docs/hooks/use_state.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/ui/docs/hooks/use_state.md b/plugins/ui/docs/hooks/use_state.md index 6c5901ff0..87c5d75d4 100644 --- a/plugins/ui/docs/hooks/use_state.md +++ b/plugins/ui/docs/hooks/use_state.md @@ -138,7 +138,11 @@ def multi_count_buttons(): result = multi_count_buttons() ``` -All of these setters will be batched together and the component will only re-render once after the event loop (the call to `increase_value`) returns. +## Multiple updates are batched + +When a component calls state setters multiple times, the updates a to state are batched. This means multiple updates to state will only trigger one re-render. + +For the example above, All of the `set_count` calls will be batched together and the component will only re-render once after the event loop (the call to `increase_value`) returns. ## Updating state with a dictionary From fe0f08b9f60da26861eb9dd2f3f36501a6e569a0 Mon Sep 17 00:00:00 2001 From: David Godinez Date: Wed, 22 Jan 2025 07:10:02 -0700 Subject: [PATCH 03/10] batches state updates --- .../add-interactivity/queueing-updates.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/plugins/ui/docs/add-interactivity/queueing-updates.md b/plugins/ui/docs/add-interactivity/queueing-updates.md index 029ed367c..5d1c99efb 100644 --- a/plugins/ui/docs/add-interactivity/queueing-updates.md +++ b/plugins/ui/docs/add-interactivity/queueing-updates.md @@ -4,4 +4,34 @@ When you set a state variable, it queues another render. However, there are time ## `deephaven.ui` batches state updates +You might expect that clicking the “+3” button will increment the counter three times because it calls `set_number(number + 1)` three times: + +```python +from deephaven import ui + + +@ui.component +def counter(): + number, set_number = ui.use_state(0) + + def handle_press(): + set_number(number + 1) + set_number(number + 1) + set_number(number + 1) + + return [ui.heading(f"{number}"), ui.button("+3", on_press=handle_press)] + + +example_counter = counter() +``` + +However, as mentioned in the previous section, the state values for each render are fixed. This means that the value of `number` within the event handler of the first render is always 0, regardless of how many times you call `set_number(number + 1)`: + +```python +def handle_press(): + set_number(0 + 1) + set_number(0 + 1) + set_number(0 + 1) +``` + TODO Arman's question!!!! From a7bb41b280a7a899161a30a42753b13cf6c45497 Mon Sep 17 00:00:00 2001 From: David Godinez Date: Wed, 22 Jan 2025 08:33:52 -0700 Subject: [PATCH 04/10] finish batches --- plugins/ui/docs/add-interactivity/queueing-updates.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/ui/docs/add-interactivity/queueing-updates.md b/plugins/ui/docs/add-interactivity/queueing-updates.md index 5d1c99efb..ec90c7998 100644 --- a/plugins/ui/docs/add-interactivity/queueing-updates.md +++ b/plugins/ui/docs/add-interactivity/queueing-updates.md @@ -34,4 +34,12 @@ def handle_press(): set_number(0 + 1) ``` +But there is another factor at play here. `deephaven.ui` waits until all code in the event handlers has run before processing your state updates. This is why the re-render only happens after all these `set_number()` calls. + +This is similar to a waiter taking an order at a restaurant. A waiter does not go to the kitchen after you order your first dish. Instead, they let you finish your order, allow you to make changes to it, and even take orders from other people at the table. + +This lets you update multiple state variables—even from multiple components—without triggering too many re-renders. But this also means that the UI will not be updated until after your event handler, and any code in it, completes. This behavior, also known as batching, makes your `deephaven.ui` app run much faster. It also avoids dealing with confusing “half-finished” renders where only some of the variables have been updated. + +## Update the same state multiple times before the next render + TODO Arman's question!!!! From 0ac8cfb97ef943834b2046afbeb3af8779eb4ecd Mon Sep 17 00:00:00 2001 From: David Godinez Date: Wed, 22 Jan 2025 09:31:57 -0700 Subject: [PATCH 05/10] update multiple --- .../add-interactivity/queueing-updates.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/plugins/ui/docs/add-interactivity/queueing-updates.md b/plugins/ui/docs/add-interactivity/queueing-updates.md index ec90c7998..a5087a422 100644 --- a/plugins/ui/docs/add-interactivity/queueing-updates.md +++ b/plugins/ui/docs/add-interactivity/queueing-updates.md @@ -42,4 +42,54 @@ This lets you update multiple state variables—even from multiple components— ## Update the same state multiple times before the next render +If you would like to update the same state variable multiple times before the next render, instead of passing the next state value like `set_number(number + 1)`, you can pass a function that calculates the next state based on the previous one in the queue, like `set_number(lambda n: n + 1)`. It is a way to tell `deephaven.ui` to “do something with the state value” instead of just replacing it. + +Try incrementing the counter now: + +```python +from deephaven import ui + + +@ui.component +def counter(): + number, set_number = ui.use_state(0) + + def handle_press(): + set_number(lambda n: n + 1) + set_number(lambda n: n + 1) + set_number(lambda n: n + 1) + + return [ui.heading(f"{number}"), ui.button("+3", on_press=handle_press)] + + +example_counter = counter() +``` + +Here, `lambda n: n + 1` is called an updater function. When you pass it to a state setter: + +1. `deephaven.ui` queues this function to be processed after all the other code in the event handler has run. +2. During the next render, `deephaven.ui` goes through the queue and gives you the final updated state. + +```python +set_number(lambda n: n + 1) +set_number(lambda n: n + 1) +set_number(lambda n: n + 1) +``` + +`deephaven.ui` adds each `lambda n: n + 1` to the queue. + +When you call `use_state` during the next render, `deephaven.ui` goes through the queue. The previous number state was 0, so that’s what `deephaven.ui` passes to the first updater function as the n argument. Then `deephaven.ui` takes the return value of your previous updater function and passes it to the next updater as n, and so on: + +| queued update | n | returns | +| ----------------- | --- | ----------- | +| `lambda n: n + 1` | `0` | `0 + 1 = 1` | +| `lambda n: n + 1` | `1` | `1 + 1 = 2` | +| `lambda n: n + 1` | `2` | `2 + 1 = 3` | + +`deephaven.ui` stores `3` as the final result and returns it from `use_state`. + +This is why clicking “+3” in the above example correctly increments the value by 3. + +## What happens if you update state after replacing it + TODO Arman's question!!!! From dfe61a445dc4aea7775002513b529da76c551f43 Mon Sep 17 00:00:00 2001 From: David Godinez Date: Wed, 22 Jan 2025 09:50:46 -0700 Subject: [PATCH 06/10] update after replacing --- .../add-interactivity/queueing-updates.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/plugins/ui/docs/add-interactivity/queueing-updates.md b/plugins/ui/docs/add-interactivity/queueing-updates.md index a5087a422..0a68b9f3b 100644 --- a/plugins/ui/docs/add-interactivity/queueing-updates.md +++ b/plugins/ui/docs/add-interactivity/queueing-updates.md @@ -92,4 +92,40 @@ This is why clicking “+3” in the above example correctly increments the valu ## What happens if you update state after replacing it +What about this event handler? What do you think number will be in the next render? + +```python +from deephaven import ui + + +@ui.component +def counter(): + number, set_number = ui.use_state(0) + + def handle_press(): + set_number(number + 5) + set_number(lambda n: n + 1) + + return [ui.heading(f"{number}"), ui.button("+3", on_press=handle_press)] + + +example_counter = counter() +``` + +Here is what this event handler tells `deephaven.ui` to do: + +1. `set_number(number + 5)`: number is `0`, so `set_number(0 + 5)`. `deephaven.ui` adds "replace with 5" to its queue. +2. `set_number(lambda n: n + 1)`: `lambda n: n + 1` is an updater function. `deephaven.ui` adds that function to its queue. + +During the next render, `deephaven.ui` goes through the state queue: + +| queued update | n | returns | +| ----------------- | ------------ | ----------- | +| "replace with 5" | `0` (unused) | `5` | +| `lambda n: n + 1` | `5` | `5 + 1 = 6` | + +`deephaven.ui` stores `6` as the final result and returns it from `use_state`. + +## What happens if you replace state after updating it + TODO Arman's question!!!! From 805de7c709f51abc2440c4802cb6128a49ccf610 Mon Sep 17 00:00:00 2001 From: David Godinez Date: Wed, 22 Jan 2025 10:17:06 -0700 Subject: [PATCH 07/10] replace after updating --- .../add-interactivity/queueing-updates.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/plugins/ui/docs/add-interactivity/queueing-updates.md b/plugins/ui/docs/add-interactivity/queueing-updates.md index 0a68b9f3b..0526afbea 100644 --- a/plugins/ui/docs/add-interactivity/queueing-updates.md +++ b/plugins/ui/docs/add-interactivity/queueing-updates.md @@ -128,4 +128,48 @@ During the next render, `deephaven.ui` goes through the state queue: ## What happens if you replace state after updating it +Let's try an example where you replace state after updating it. What do you think number will be in the next render? + +```python +from deephaven import ui + + +@ui.component +def counter(): + number, set_number = ui.use_state(0) + + def handle_press(): + set_number(number + 5) + set_number(lambda n: n + 1) + set_number(42) + + return [ui.heading(f"{number}"), ui.button("+3", on_press=handle_press)] + + +example_counter = counter() +``` + +Here is how `deephaven.ui` works through these lines of code while executing this event handler: + +1. `set_number(number + 5)`: number is `0`, so `set_number(0 + 5)`. `deephaven.ui` adds "replace with 5" to its queue. +2. `set_number(lambda n: n + 1)`: `lambda n: n + 1` is an updater function. `deephaven.ui` adds that function to its queue. +3. `set_number(42)`: `deephaven.ui` adds "replace with 42" to its queue. + +| queued update | n | returns | +| ----------------- | ------------ | ----------- | +| "replace with 5" | `0` (unused) | `5` | +| `lambda n: n + 1` | `5` | `5 + 1 = 6` | +| "replace with 42" | `6` (unused) | `42` | + +Then `deephaven.ui` stores `42` as the final result and returns it from `use_state`. + +To summarize, here is how you can think of what you are passing to the `set_number` state setter: + +1. An updater function (e.g. `lambda n: n + 1`) gets added to the queue. +2. Any other value (e.g. number `5`) adds "replace with 5" to the queue, ignoring what’s already queued. + +After the event handler completes, `deephaven.ui` will trigger a re-render. During the re-render, `deephaven.ui` will process the queue. Updater functions run during rendering, so updater functions must be pure and only return the result. Do not try to set state from inside of them or run other side effects. + +## Naming conventions + TODO Arman's question!!!! From 2210c2b717d5a72c51fdbd84165e133b34e3a91a Mon Sep 17 00:00:00 2001 From: David Godinez Date: Wed, 22 Jan 2025 10:30:29 -0700 Subject: [PATCH 08/10] naming conventions --- plugins/ui/docs/add-interactivity/queueing-updates.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/ui/docs/add-interactivity/queueing-updates.md b/plugins/ui/docs/add-interactivity/queueing-updates.md index 0526afbea..c20ce3e79 100644 --- a/plugins/ui/docs/add-interactivity/queueing-updates.md +++ b/plugins/ui/docs/add-interactivity/queueing-updates.md @@ -172,4 +172,12 @@ After the event handler completes, `deephaven.ui` will trigger a re-render. Duri ## Naming conventions -TODO Arman's question!!!! +It is common to name the updater function argument by the first letters of the corresponding state variable: + +```python +set_enabled(lambda e: not e) +set_last_name(lambda ln: ln.upper()) +set_friend_count(lambda fc: fc * 2) +``` + +If you prefer more verbose code, another common convention is to repeat the full state variable name, like `set_enabled(lambda enabled: not enabled)`, or to use a prefix like `set_enabled(lambda prev_enabled: not prev_enabled)`. From a901c0f1ce20e0eab3861dbe3eb2d8fc956240a9 Mon Sep 17 00:00:00 2001 From: dgodinez-dh <77981300+dgodinez-dh@users.noreply.github.com> Date: Mon, 27 Jan 2025 12:30:59 -0700 Subject: [PATCH 09/10] Update plugins/ui/docs/hooks/use_state.md Co-authored-by: Mike Bender --- plugins/ui/docs/hooks/use_state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ui/docs/hooks/use_state.md b/plugins/ui/docs/hooks/use_state.md index 87c5d75d4..3044eb1ef 100644 --- a/plugins/ui/docs/hooks/use_state.md +++ b/plugins/ui/docs/hooks/use_state.md @@ -140,7 +140,7 @@ result = multi_count_buttons() ## Multiple updates are batched -When a component calls state setters multiple times, the updates a to state are batched. This means multiple updates to state will only trigger one re-render. +When a component calls state setters multiple times, the updates to state are batched. This means multiple updates to state will only trigger one re-render. For the example above, All of the `set_count` calls will be batched together and the component will only re-render once after the event loop (the call to `increase_value`) returns. From d36aa564406be5438a95d58056071b9af7f02e7e Mon Sep 17 00:00:00 2001 From: David Godinez Date: Mon, 27 Jan 2025 12:39:54 -0700 Subject: [PATCH 10/10] multi-threaded --- plugins/ui/docs/add-interactivity/queueing-updates.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/ui/docs/add-interactivity/queueing-updates.md b/plugins/ui/docs/add-interactivity/queueing-updates.md index c20ce3e79..2990f0249 100644 --- a/plugins/ui/docs/add-interactivity/queueing-updates.md +++ b/plugins/ui/docs/add-interactivity/queueing-updates.md @@ -40,6 +40,8 @@ This is similar to a waiter taking an order at a restaurant. A waiter does not g This lets you update multiple state variables—even from multiple components—without triggering too many re-renders. But this also means that the UI will not be updated until after your event handler, and any code in it, completes. This behavior, also known as batching, makes your `deephaven.ui` app run much faster. It also avoids dealing with confusing “half-finished” renders where only some of the variables have been updated. +Note that in multi-threaded cases, state updates are not batched by default. You can use the [use_render_queue](../hooks/use_render_queue.md) to ensure they do get batched if you are going to do work from a background thread. See [`batch-updates`](../hooks/use_render_queue.md#batch-updates) for more information. + ## Update the same state multiple times before the next render If you would like to update the same state variable multiple times before the next render, instead of passing the next state value like `set_number(number + 1)`, you can pass a function that calculates the next state based on the previous one in the queue, like `set_number(lambda n: n + 1)`. It is a way to tell `deephaven.ui` to “do something with the state value” instead of just replacing it.