Skip to content

Commit

Permalink
docs: use_render_queue, use_liveness_scope, use_table_listener docs
Browse files Browse the repository at this point in the history
- Adding some more missing hooks documentation
- Closes #823, closes #659
  • Loading branch information
mofojed committed Nov 26, 2024
1 parent e53b322 commit 4dd2145
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 7 deletions.
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

`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.

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.

## Example

```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.
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

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:

```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.

## 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.

## 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.
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

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:

```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).

## 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.

## 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).

## 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.

```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),
)

0 comments on commit 4dd2145

Please sign in to comment.