Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: use_render_queue, use_liveness_scope, use_table_listener docs #1044

Merged
merged 20 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed plugins/ui/docs/_assets/change_monitor.png
Binary file not shown.
7 changes: 4 additions & 3 deletions plugins/ui/docs/components/toast.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ my_mount_example = ui_toast_on_mount()

## Toast from table example

This example shows how to create a toast from the latest update of a ticking table. It is recommended to auto dismiss these toasts with a `timeout` and to avoid ticking faster than the value of the `timeout`.
This example shows how to create a toast from the latest update of a ticking table. It is recommended to auto dismiss these toasts with a `timeout` and to avoid ticking faster than the value of the `timeout`. Note that the toast must be triggered on the render thread, whereas the table listener may be fired from another thread. Therefore you must use the render queue to trigger the toast.

```python
from deephaven import time_table
Expand All @@ -123,7 +123,7 @@ def toast_table(t):
data_added = update.added()["X"][0]
render_queue(lambda: ui.toast(f"added {data_added}", timeout=5000))

ui.use_table_listener(t, listener_function, [t])
ui.use_table_listener(t, listener_function, [])
return t


Expand All @@ -141,7 +141,8 @@ from deephaven import read_csv, ui

@ui.component
def csv_loader():
# The render_queue we fetch using the `use_render_queue` hook at the top of the component
# The render_queue we fetch using the `use_render_queue` hook at the top of the component.
# The toast must be triggered from the render queue.
render_queue = ui.use_render_queue()
table, set_table = ui.use_state()
error, set_error = ui.use_state()
Expand Down
62 changes: 62 additions & 0 deletions plugins/ui/docs/hooks/use_liveness_scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# use_liveness_scope
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for @dsmmcken: When Salmon goes live, will these headings have to change the docs syntax:

---
id: whatever
title: whatever
sidebar_label: ...
---

And if so, is there a transition tool that will make changing them quick?


`use_liveness_scope` allows your to interact with the [liveness scope](https://deephaven.io/core/docs/conceptual/liveness-scope-concept/) for a component. Some functions which interact with a component will create live objects that need to be managed by the component to ensure they are kept active.
mofojed marked this conversation as resolved.
Show resolved Hide resolved

The primary use case for this is when creating tables outside the component's own function, and passing them as state for the component's next update. If the table is not kept alive by the component, it will be garbage collected and the component will not be able to update with the new data.
mofojed marked this conversation as resolved.
Show resolved Hide resolved

## Example
mofojed marked this conversation as resolved.
Show resolved Hide resolved

```python
from deephaven import ui, time_table


@ui.component
def ui_resetable_table():
table, set_table = ui.use_state(lambda: time_table("PT1s"))
handle_press = ui.use_liveness_scope(lambda _: set_table(time_table("PT1s")), [])
return [
ui.action_button(
"Reset",
on_press=handle_press,
),
table,
]


resetable_table = ui_resetable_table()
```

## UI recommendations

1. **Avoid using `use_liveness_scope` unless necessary**: This is an advanced feature that should only be used when you need to manage the liveness of objects outside of the component's own function. Prefer instead to derive a live component based on state rather than setting a live component within state.
mofojed marked this conversation as resolved.
Show resolved Hide resolved
2. **Use `use_liveness_scope` to manage live objects**: If you need to manage the liveness of objects created outside of the component's own function, use `use_liveness_scope` to ensure they are kept alive. For more information on liveness scopes and why they are needed, see the [liveness scope documentation](https://deephaven.io/core/docs/conceptual/liveness-scope-concept/).

## Refactoring to avoid liveness scope
mofojed marked this conversation as resolved.
Show resolved Hide resolved

In the above example, we could refactor the component to avoid using `use_liveness_scope` by deriving the table from state instead of setting it directly:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which is the better approach? If this is better than using a liveness scope, then why present the first example at all?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's kind of an escape hatch if you need it. Ideally you refactor such that you don't need to use it.


```python
from deephaven import ui, time_table


@ui.component
def ui_resetable_table():
iteration, set_iteration = ui.use_state(0)
table = ui.use_memo(lambda: time_table("PT1s"), [iteration])
return [
ui.action_button(
"Reset",
on_press=lambda: set_iteration(iteration + 1),
),
table,
]


resetable_table = ui_resetable_table()
```

## API Reference

```{eval-rst}
.. dhautofunction:: deephaven.ui.use_liveness_scope
```
148 changes: 148 additions & 0 deletions plugins/ui/docs/hooks/use_render_queue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# use_render_queue

`use_render_queue` lets you use the render queue in your component. This is useful when you want to queue updates on the render thread from a background thread.
mofojed marked this conversation as resolved.
Show resolved Hide resolved

## Example

```python
from deephaven import time_table
from deephaven import ui

_source = time_table("PT5S").update("X = i").tail(5)


@ui.component
def toast_table(t):
render_queue = ui.use_render_queue()

def listener_function(update, is_replay):
data_added = update.added()["X"][0]
render_queue(lambda: ui.toast(f"added {data_added}", timeout=5000))

ui.use_table_listener(t, listener_function, [])
return t


my_toast_table = toast_table(_source)
```

The above example listens to table updates and displays a toast message when the table updates. The `toast` function must be triggered on the render thread, whereas the listener is not fired on the render thread. Therefore, you must use the render queue to trigger the toast.
mofojed marked this conversation as resolved.
Show resolved Hide resolved

## UI recommendations

1. **Use the render queue to trigger toasts**: When you need to trigger a toast from a background thread, use the render queue to ensure the toast is triggered on the render thread. Otherwise, an exception will be raised.
mofojed marked this conversation as resolved.
Show resolved Hide resolved
2. **Use the render queue to batch UI updates from a background thread**: By default, setter functions from the `use_state` hook are already fired on the render thread. However, if you have multiple updates to make to the UI from a background thread, you can use the render queue to batch them together.

## Batching updates
mofojed marked this conversation as resolved.
Show resolved Hide resolved

Setter functions from the `use_state` hook are fired on the render thread, so if you call a series of updates from a callback on the render thread, they will be batched together. Consider the following, which will increment states `a` and `b` in the callback from pressing on "Update values":

```python
from deephaven import ui
import time


@ui.component
def ui_batch_example():
a, set_a = ui.use_state(0)
b, set_b = ui.use_state(0)

ui.toast(
f"Values are {a} and {b}",
variant="negative" if a != b else "neutral",
timeout=5000,
)

def do_work():
set_a(lambda new_a: new_a + 1)
# Introduce a bit of delay between updates
time.sleep(0.1)
set_b(lambda new_b: new_b + 1)

return ui.button("Update values", on_press=do_work)


batch_example = ui_batch_example()
```

Because `do_work` is called from the render thread (in response to the `on_press` ), `set_a` and `set_b` will queue their updates on the render thread and they will be batched together. This means that the toast will only show once, with the updated values of `a` and `b` and they will always be the same value when the component re-renders.

If we instead put `do_work` in a background thread, the updates are not guaranteed to be batched together:

```python
from deephaven import ui
import threading
import time


@ui.component
def ui_batch_example():
a, set_a = ui.use_state(0)
b, set_b = ui.use_state(0)

ui.toast(
f"Values are {a} and {b}",
variant="negative" if a != b else "neutral",
timeout=5000,
)

def do_work():
set_a(lambda new_a: new_a + 1)
# Introduce a bit of delay between updates
time.sleep(0.1)
set_b(lambda new_b: new_b + 1)

def start_background_thread():
threading.Thread(target=do_work).start()

return ui.button("Update values", on_press=start_background_thread)


batch_example = ui_batch_example()
```

When running the above example, we'll see _two_ toasts with each press of the button: a red one where `a != b` (as `a` gets updated first), then a neutral one where `a == b` (as `b` gets updated second). We can use the `use_render_queue` hook to ensure the updates are always batched together when working with a background thread:
mofojed marked this conversation as resolved.
Show resolved Hide resolved

```python
from deephaven import ui
import threading
import time


@ui.component
def ui_batch_example():
render_queue = ui.use_render_queue()
a, set_a = ui.use_state(0)
b, set_b = ui.use_state(0)

ui.toast(
f"Values are {a} and {b}",
variant="negative" if a != b else "neutral",
timeout=5000,
)

def do_work():
def update_state():
set_a(lambda new_a: new_a + 1)
# Introduce a bit of delay between updates
time.sleep(0.1)
set_b(lambda new_b: new_b + 1)

render_queue(update_state)

def start_background_thread():
threading.Thread(target=do_work).start()

return ui.button("Update values", on_press=start_background_thread)


batch_example = ui_batch_example()
```

Now when we run this example and press the button, we'll see only one toast with the updated values of `a` and `b`, and they will always be the same value when the component re-renders (since the updates are batched together on the render thread).
mofojed marked this conversation as resolved.
Show resolved Hide resolved

## API Reference

```{eval-rst}
.. dhautofunction:: deephaven.ui.use_render_queue
```
84 changes: 84 additions & 0 deletions plugins/ui/docs/hooks/use_table_listener.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# use_table_listener

`use_table_listener` lets you listen to a table for updates. This is useful when you want to listen to a table and perform an action when the table updates.
mofojed marked this conversation as resolved.
Show resolved Hide resolved

## Example

```python
from deephaven import time_table, ui
from deephaven.table import Table

_source = time_table("PT1s").update("X = i")


@ui.component
def ui_table_monitor(t: Table):
def listener_function(update, is_replay):
print(f"Table updated: {update}, is_replay: {is_replay}")

ui.use_table_listener(t, listener_function, [])
return t


table_monitor = ui_table_monitor(_source)
```

## UI recommendations

1. **Use table data hooks instead when possible**: `use_table_listener` is an advanced feature, requiring understanding of how the [table listeners](https://deephaven.io/core/docs/how-to-guides/table-listeners-python/) work, and limitations of running code while the Update Graph is running. Most usages of this are more appropriate to implement with [the table data hooks](./overview.md#data-hooks).
mofojed marked this conversation as resolved.
Show resolved Hide resolved

## Display the last updated row

Here's an example that listens to table updates and will display the last update as a header above the table. This is a simple example to demonstrate how to use `use_table_listener` to listen to table updates and update state in your component.

```python
from deephaven import time_table, ui
from deephaven.table import Table


@ui.component
def ui_show_last_changed(t: Table):
last_change, set_last_change = ui.use_state("No changes yet.")

def listener_function(update, is_replay):
set_last_change(f"{update.added()['X'][0]} was added")

ui.use_table_listener(t, listener_function, [])
return [ui.heading(f"Last change: {last_change}"), t]


_source = time_table("PT5s").update("X = i")
show_last_changed = ui_show_last_changed(_source)
```

## Display a toast

Here's an example that listens to table updates and will display a toast message when the table updates. This is a simple example to demonstrate how to use `use_table_listener` to listen to table updates and display a toast message. Note you must use a [render queue](./use_render_queue.md) to trigger the toast, as the listener is not fired on the render thread.
mofojed marked this conversation as resolved.
Show resolved Hide resolved

```python
from deephaven import time_table
from deephaven import ui

_source = time_table("PT5S").update("X = i").tail(5)


@ui.component
def toast_table(t):
render_queue = ui.use_render_queue()

def listener_function(update, is_replay):
data_added = update.added()["X"][0]
render_queue(lambda: ui.toast(f"added {data_added}", timeout=5000))

ui.use_table_listener(t, listener_function, [t])
return t


my_toast_table = toast_table(_source)
```

## API Reference

```{eval-rst}
.. dhautofunction:: deephaven.ui.use_table_listener
```
4 changes: 2 additions & 2 deletions plugins/ui/src/deephaven/ui/_internal/EventContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ def get_event_context() -> EventContext:
"""
try:
return _local_data.event_context
except AttributeError:
raise NoContextException("No context set")
except AttributeError as e:
raise NoContextException("No context set") from e


def _set_event_context(context: Optional[EventContext]):
Expand Down
12 changes: 11 additions & 1 deletion plugins/ui/src/deephaven/ui/components/toast.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@

from typing import Callable
from .._internal.utils import dict_to_react_props
from .._internal.EventContext import NoContextException
from ..types import ToastVariant

_TOAST_EVENT = "toast.event"


class ToastException(NoContextException):
pass


def toast(
message: str,
*,
Expand Down Expand Up @@ -37,5 +42,10 @@ def toast(
None
"""
params = dict_to_react_props(locals())
send_event = use_send_event()
try:
send_event = use_send_event()
except NoContextException as e:
raise ToastException(
"Toasts must be triggered from the render thread. Use the hook `use_render_queue` to queue a function on the render thread."
) from e
send_event(_TOAST_EVENT, params)
2 changes: 1 addition & 1 deletion plugins/ui/src/deephaven/ui/hooks/use_table_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,5 @@ def start_listener() -> Callable[[], None]:

use_effect(
start_listener,
[table, listener, description, do_replay] + list(dependencies),
[table, description, do_replay] + list(dependencies),
mofojed marked this conversation as resolved.
Show resolved Hide resolved
)
Loading