Skip to content

Commit

Permalink
docs: warn users more strongly against using mo.state() and on_change…
Browse files Browse the repository at this point in the history
… handlers. (#3266)

Too many of our users fall into callback driven programming which is
very ill-suited to marimo. This PR updates the docs to more strongly
warn users against using mo.state()/on_change.
  • Loading branch information
akshayka authored Dec 21, 2024
1 parent 8ea65f0 commit 90f55be
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 40 deletions.
27 changes: 13 additions & 14 deletions docs/api/state.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
# State

!!! warning "Advanced topic!"
!!! warning "Stop! Read the interactivity guide first!"
**Read the guide on [creating interactive
elements](../guides/interactivity.md)** before reading this one!

This API doc covers reactive state (`mo.state`), an advanced topic.
!!! warning "Advanced topic!"
This guide covers reactive state (`mo.state`), an advanced topic.

**You likely don't need reactive state**. UI elements already have built-in
**You likely don't need `mo.state`**. UI elements already have built-in
state, their associated value, which you can access with their `value` attribute.
For example, `mo.ui.slider()` has a value that is its current position on an
interval, while `mo.ui.button()` has a value that can be configured to
count the number of times it has been clicked, or to toggle between `True` and
`False`. Additionally, interacting with UI elements bound to global variables
[automatically executes cells](../guides/interactivity.md) that reference those
variables, letting you react to changes by just reading their
`value` attributes. This functional paradigm is the preferred way of
reacting to UI interactions in marimo. So if you
think you need to use `mo.state`, make sure to first read the [guide on
interactivity](../guides/interactivity.md). Chances are, the reactive execution
built into UI elements will suffice. (For example, [you don't need reactive
state to handle a button click](../recipes.md#working-with-buttons).)

`value` attributes. **This functional paradigm is the preferred way of
reacting to UI interactions in marimo.** **Chances are, the reactive
execution built into UI elements will suffice.** (For example, [you don't need
reactive state to handle a button click](../recipes.md#working-with-buttons).)

That said, here are some signs you might need `mo.state`:

- you need to maintain historical state related to a UI element that can't
be computed from its built-in `value` (_e.g._, all values the user has
ever input into a form)
- you need to synchronize two different UI elements (_e.g._, so that
interacting with either one controls the other)
- you need to introduce cycles across cells

If one of these cases applies to you, then read on. `mo.state` lets you
make all kinds of interesting applications, but like mutable state in general,
it can complicate notebook development and has the potential to
introduce hard-to-find bugs.
**In over 99% of cases, you don't need and shouldn't use `mo.state`.** This
feature can introduce hard-to-find bugs.

::: marimo.state
3 changes: 3 additions & 0 deletions docs/guides/best_practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ instead of mutating existing ones.
```python
extended_list = l + [new_item()]
```
**Don't use state and `on_change` handlers.** Don't use `on_change` handlers
to react to UI interactions. Instead, use marimo's built-in [reactive execution
for interactive elements](../guides/interactivity.md).

**Write idempotent cells.**
Write cells whose outputs and behavior are the same
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/interactivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ assigned to a global variable.**

Display UI elements in the output area above a cell by including them in the
last expression, just like any other object. You can also embed elements
in [markdown][marimo.md]using Python f-strings, like so:
in [markdown][marimo.md] using Python f-strings, like so:

```python3
slider = mo.ui.slider(1, 10)
Expand Down
42 changes: 24 additions & 18 deletions docs/guides/state.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
# Reactive state

!!! warning "Stop! Read the interactivity guide first!"
**Read the guide on [creating interactive
elements](../guides/interactivity.md)** before reading this one!

!!! warning "Advanced topic!"
This guide covers reactive state (`mo.state`), an advanced topic.

**You likely don't need reactive state**. UI elements already have built-in
**You likely don't need `mo.state`**. UI elements already have built-in
state, their associated value, which you can access with their `value` attribute.
For example, `mo.ui.slider()` has a value that is its current position on an
interval, while `mo.ui.button()` has a value that can be configured to
count the number of times it has been clicked, or to toggle between `True` and
`False`. Additionally, interacting with UI elements bound to global variables
[automatically executes cells](../guides/interactivity.md) that reference those
variables, letting you react to changes by just reading their
`value` attributes. This functional paradigm is the preferred way of
reacting to UI interactions in marimo. So if you
think you need to use `mo.state`, make sure to first read the [guide on
interactivity](../guides/interactivity.md). Chances are, the reactive execution
built into UI elements will suffice. (For example, [you don't need reactive
state to handle a button click](../recipes.md#working-with-buttons).)

`value` attributes. **This functional paradigm is the preferred way of
reacting to UI interactions in marimo.** **Chances are, the reactive
execution built into UI elements will suffice.** (For example, [you don't need
reactive state to handle a button click](../recipes.md#working-with-buttons).)

That said, here are some signs you might need `mo.state`:

- you need to maintain historical state related to a UI element that can't
be computed from its built-in `value` (_e.g._, all values the user has
ever input into a form)
- you need to synchronize two different UI elements (_e.g._, so that
interacting with either one controls the other)
- you need to introduce cycles across cells

If one of these cases applies to you, then read on. `mo.state` lets you make
all kinds of interesting applications, but like mutable state in general,
it can complicate notebook development and has the potential to
introduce hard-to-find bugs.
**In over 99% of cases, you don't need and shouldn't use `mo.state`.** This
feature can introduce hard-to-find bugs.

You can build powerful, interactive notebooks and apps using just `mo.ui` and
reactivity.
Expand All @@ -47,16 +47,21 @@ But sometimes, you might want interactions to mutate state:
<figcaption>A proof-of-concept TODO list made using state.</figcaption>
</div>

- You want to tie two different UI elements so that updating one updates
the other.
- You want to tie two different UI elements so that updating **either** one
updates the other.

<div align="center" style="margin-top:2rem; margin-bottom:2rem">
<figure>
<img src="/_static/docs-state-tied.gif"/>
<figcaption>Use state to tie two elements together.</figcaption>
<figcaption>Use state to tie two elements together in a cycle.</figcaption>
</figure>
</div>

!!! warning "Use reactive execution for uni-directional flow"
If you just want the value of a single element to update another element,
then **you shouldn't use `mo.state`**. Instead, use marimo's built-in
reactive execution --- see the [interactivity guide](`../guides/interactivity.md`).

For cases like these, marimo provides the function [`mo.state()`](../api/state.md),
which creates a state object and returns a getter and setter function. When you
call the setter function in one cell, all other cells that reference the getter
Expand Down Expand Up @@ -135,9 +140,10 @@ the new value of the element and does anything with it. You can use the setter
function in an `on_change` callback to mutate state.

!!! note "Use state sparingly"
You can get far using just `mo.ui`, without state. But judiciously using
state can simplify the implementation of highly interactive notebooks/apps, and
also enables new use cases. The next few examples showcase good uses of state.
You can get far using just `mo.ui`, without state, because marimo
automatically runs cells that reference UI elements on interaction
(see the [interactivity guide](../guides/interactivity.md)). Only
use `on_change` callbacks as a last resort!

### Example: counter

Expand Down
24 changes: 18 additions & 6 deletions marimo/_runtime/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,16 @@ def state(
"""
Mutable reactive state.
Warning: reactive state is an advanced feature that you likely don't need;
it makes it possible to introduce cycles and hard-to-debug code execution
paths. **In almost all cases, you should prefer using marimo's built-in
[reactive execution](https://docs.marimo.io/guides/reactivity) and
[interactivity](https://docs.marimo.io/guides/interactivity).**
This function takes an initial value and returns:
- a getter function that reads the state value
- a setter function to set the state's value
- a getter function that reads the state value
- a setter function to set the state's value
When you call the setter function and update the state value in one cell,
all *other* cells that read any global variables assigned to the getter
Expand All @@ -185,10 +192,14 @@ def state(
`allow_self_loops=True`.
You can use this function with `UIElement` `on_change` handlers to trigger
side-effects when an element's value is updated. For example, you can tie
multiple UI elements to derive their values from shared state.
side-effects when an element's value is updated; however, you should
prefer using marimo's built-in [reactive execution for interactive
elements](https://docs.marimo.io/guides/interactivity).
For example, you can tie multiple UI elements to derive their values from
shared state.
Example usage:
Examples:
Create state:
```python
get_count, set_count = mo.state(0)
Expand All @@ -206,7 +217,8 @@ def state(
set_count(lambda value: value + 1)
```
Never mutate the state directly. You should only change its value through its setter.
Never mutate the state directly. You should only change its value through
its setter.
**Synchronizing multiple UI elements:**
```python
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ nav:
- Overview: guides/integrating_with_marimo/index.md
- Custom UI plugins: guides/integrating_with_marimo/custom_ui_plugins.md
- Displaying objects: guides/integrating_with_marimo/displaying_objects.md
- State management: guides/state.md
- Best practices: guides/best_practices.md
- Troubleshooting: guides/troubleshooting.md
- Dangerously set state: guides/state.md
- API Reference:
- Overview: api/index.md
- Markdown: api/markdown.md
Expand Down

0 comments on commit 90f55be

Please sign in to comment.