Skip to main content

State Management

marimo provides mo.state() for managing mutable reactive state in your notebooks. While marimo’s built-in reactivity handles most use cases, state() enables advanced patterns like synchronized UI elements and side effects.
Reactive state is an advanced feature that can introduce cycles and hard-to-debug execution paths. In almost all cases, you should prefer using marimo’s built-in reactive execution and interactivity.

Understanding marimo.state()

The mo.state() function creates mutable reactive state that triggers automatic re-execution of dependent cells when updated.

Basic Usage

import marimo as mo

# Create state with initial value
get_count, set_count = mo.state(0)
This returns:
  • Getter function: Reads the current state value
  • Setter function: Updates the state value and triggers reactivity

Reading State

# In another cell
current_value = get_count()
mo.md(f"Count: {current_value}")

Updating State

# Set to a specific value
set_count(42)

Reactivity Behavior

When you call a state setter:
  1. The state value is updated
  2. All other cells that reference the getter are automatically re-run
  3. By default, the cell that called the setter is not re-run (preventing infinite loops)

Self-Loops

To allow a cell to re-run itself when calling the setter:
get_state, set_state = mo.state(0, allow_self_loops=True)

# This cell will re-run until x reaches 3
x = get_state()
if x < 3:
    set_state(x + 1)
Use allow_self_loops=True carefully - ensure your logic has a termination condition to prevent infinite loops.

Common Use Cases

Synchronizing Multiple UI Elements

Bind multiple UI elements to shared state so they stay synchronized:
# Cell 1: Create shared state
get_value, set_value = mo.state(50)
# Cell 2: Create slider bound to state
slider = mo.ui.slider(
    0, 100,
    value=get_value(),
    on_change=set_value
)
slider
# Cell 3: Create number input bound to same state
number = mo.ui.number(
    0, 100,
    value=get_value(),
    on_change=set_value
)
number
When either element is updated, both will reflect the new value automatically.

Tracking User Interactions

# Track click count
get_clicks, set_clicks = mo.state(0)

button = mo.ui.button(
    label="Click me",
    on_click=lambda _: set_clicks(lambda n: n + 1)
)

mo.md(f"Clicked {get_clicks()} times")

Building State Machines

get_status, set_status = mo.state("idle")

# Different actions based on current state
if get_status() == "idle":
    start_btn = mo.ui.button(
        "Start",
        on_click=lambda _: set_status("running")
    )
elif get_status() == "running":
    stop_btn = mo.ui.button(
        "Stop",
        on_click=lambda _: set_status("idle")
    )

Implementation Details

State Registry

marimo maintains a state registry that tracks all state instances in your notebook. From marimo/_runtime/state.py:
class StateRegistry:
    def __init__(self) -> None:
        # variable name -> state
        self._states: dict[str, StateItem[Any]] = {}
        # id -> variable name for state
        self._inv_states: dict[Id, set[str]] = {}
The registry:
  • Uses weak references to avoid memory leaks
  • Prunes inactive states when cells are deleted
  • Maintains bidirectional mappings for efficient lookups

SetFunctor

The setter is implemented as a typed functor that handles both direct values and functional updates:
class SetFunctor(Generic[T]):
    def __call__(self, update: T | Callable[[T], T]) -> None:
        self._state._value = (
            update(self._state._value)
            if isinstance(update, (types.MethodType, types.FunctionType))
            else update
        )
        ctx.register_state_update(self._state)

Best Practices

State is ideal for keeping multiple UI elements in sync or managing UI-driven side effects.
Never store marimo.ui elements in state - this can cause hard-to-diagnose bugs and breaks reactivity.
# ❌ Don't do this
get_widget, set_widget = mo.state(mo.ui.slider(0, 10))

# ✓ Do this instead
get_value, set_value = mo.state(5)
widget = mo.ui.slider(0, 10, value=get_value())
For most cases, marimo’s automatic reactivity is sufficient:
# Usually this is all you need
slider = mo.ui.slider(0, 100)

# In another cell, this automatically updates when slider changes
result = slider.value * 2
Always use the setter function - never mutate the state value directly.
# ❌ Don't do this
get_list, set_list = mo.state([1, 2, 3])
get_list().append(4)  # Bad!

# ✓ Do this instead
set_list(lambda lst: lst + [4])

Testing State

From the test suite (tests/_runtime/test_state.py):
import marimo as mo

# Test basic state update
state, set_state = mo.state(0)
assert state() == 0

set_state(1)
assert state() == 1

# Test functional update
set_state(lambda v: v + 1)
assert state() == 2

# Test with callable objects (not functions)
class Counter:
    called = False
    def __call__(self):
        self.called = True

counter = Counter()
set_state(counter)  # Sets state to counter instance
assert state() == counter
assert not counter.called  # Not called as a function

When to Use State

Use mo.state() when:
  • Synchronizing multiple UI elements to the same value
  • Implementing complex UI interaction patterns
  • Building state machines or multi-step workflows
  • Triggering side effects from UI interactions
Don’t use mo.state() when:
  • Simple variable assignment works (use marimo’s built-in reactivity)
  • You’re just reading UI element values (use .value directly)
  • You need to store computation results (use regular variables)

Build docs developers (and LLMs) love