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: Using Hooks #1056

Merged
merged 18 commits into from
Dec 16, 2024
186 changes: 186 additions & 0 deletions plugins/ui/docs/describing/use_hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Use Hooks

Hooks are Python functions that isolate reusable parts of a component. Built-in `deephaven.ui` hooks allow you to manage state, cache values, synchronize with external systems, and much more. You can either use the built-in hooks or combine them to build your own.

## Rules for Hooks

Hooks are Python functions, but you need to follow two rules when using them.

1. Only call hooks at the top level.

Don’t call hooks inside loops, conditions, or nested functions. Instead, always use hooks at the top level of your `deephaven.ui` component function, before any early returns. By following this rule, you ensure that hooks are called in the same order each time a component renders.

2. Only call hooks from components and custom hooks

Don’t call hooks from regular Python functions. Instead, you can:

- Call Hooks from `@ui.component` decorated functions.
- Call hooks from custom hooks.

Following this rule ensures that all stateful logic in a component is clearly visible from its source code.

## Built-in Hooks

`deephaven.ui` has a large number of built-in hooks to help with the development of components. More details can be found in the [`hooks` section](../hooks/overview.md) of the documentation.

### Use State Hook

Call [`use_state`](../hooks/use_state.md) at the top level of your component to declare a state variable.

```python
from deephaven import ui


@ui.component
def ui_counter():
count, set_count = ui.use_state(0)
return ui.button(f"Pressed {count} times", on_press=lambda: set_count(count + 1))


counter = ui_counter()
```

The `use_state` hook takes an optional parameter, the initial state. If this is omitted, it initializes to `None`. The hook returns two values: a state variable and a `set` function that lets you update the state and trigger a re-render.

### Use Memo Hook

Call [`use_memo`](../hooks/use_memo.md) to cache the result of a calculation, function, or operation. This is useful when you have a value that is expensive to compute and you want to avoid re-computing it on every render.

```python
from deephaven import ui


@ui.component
def ui_todo_list(todos: list[str], filter: str):
filtered_todos = ui.use_memo(
lambda: [todo for todo in todos if filter in todo], [todos, filter]
)

return [
ui.text(f"Showing {len(filtered_todos)} of {len(todos)} todos"),
*[ui.checkbox(todo) for todo in filtered_todos],
]


result = ui_todo_list(["Do grocery shopping", "Walk the dog", "Do laundry"], "Do")
```

The `use_memo` hook takes two parameters: a `callable` that returns a value and a list of dependencies. When dependencies are changed, the value is computed once and then stored in the memoized value. The memoized value is returned on subsequent renders until the dependencies change. The memoized value is returned on subsequent renders until the dependencies change.

### Use Effect Hook

Call [`use_effect`](../hooks/use_effect.md) to synchronize a component with an external system. An effect runs when it is mounted or a dependency changes. An optional cleanup function runs when dependencies change or the component is unmounted.

```python
from deephaven import ui


@ui.component
def ui_effect_example():
def handle_mount():
# effect prints "Mounted" once when component is first rendered
print("Mounted")
# cleanup function prints "Unmounted" when component is closed
return lambda: print("Unmounted")

# Passing in an empty list for dependencies will run the effect only once when the component is mounted, and cleanup when the component is unmounted
ui.use_effect(handle_mount, [])

return ui.text("Effect Example")


effect_example = ui_effect_example()
```

The `use_effect` hook takes two parameters: a callable and a list of dependencies. The callable may return a function for cleanup.

### Use Callback Hook

Call [`use_callback`](../hooks/use_callback.md) to memoize a callback function. This prevents unnecessary re-renders when the dependencies of the callback have not changed.

```python
from deephaven import ui
import time


@ui.component
def ui_server():
theme, set_theme = ui.use_state("red")

create_server = ui.use_callback(lambda: {"host": "localhost"}, [])

def connect():
server = create_server()
print(f"Connecting to {server}")
time.sleep(0.5)

ui.use_effect(connect, [create_server])

return ui.view(
ui.picker(
"red",
"orange",
"yellow",
label="Theme",
selected_key=theme,
on_change=set_theme,
),
padding="size-100",
background_color=theme,
)


my_server = ui_server()
```

The `use_callback` hook takes two parameters: a callable and a list of dependencies. It returns a memoized callback. The memoized callback is returned on subsequent renders until the dependencies change.

## Build your own hooks

When you have reusable logic involving one or more hooks, you may want to write a custom hook to encapsulate that logic. A hook is a Python function that follows these guidelines:
margaretkennedy marked this conversation as resolved.
Show resolved Hide resolved

- Hooks can call other hooks, but usage of hooks within hooks follows the same rules as using hooks within components.
- Custom hooks should start with the word `use` to indicate that is a hook and may contain component state and effects.

### Example: Extracting the `use_server` hook

Look back at the code example for the `use_callback` hook. The component uses two hooks to connect to a server. This logic can be extracted into a `use_server` hook to make it reusable by other components.

```python
from deephaven import ui
import time

# Custom hook
def use_server():
create_server = ui.use_callback(lambda: {"host": "localhost"}, [])

def connect():
server = create_server()
print(f"Connecting to {server}")
time.sleep(0.5)

ui.use_effect(connect, [create_server])


@ui.component
def ui_server():
theme, set_theme = ui.use_state("red")

use_server()

return ui.view(
ui.picker(
"red",
"orange",
"yellow",
label="Theme",
selected_key=theme,
on_change=set_theme,
),
padding="size-100",
background_color=theme,
)


my_server = ui_server()
```
4 changes: 4 additions & 0 deletions plugins/ui/docs/sidebar.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"label": "Component Rules",
"path": "describing/component_rules.md"
},
{
"label": "Using Hooks",
"path": "describing/use_hooks.md"
},
{
"label": "Working with Tables",
"path": "describing/work_with_tables.md"
Expand Down
Loading