Claude
Skills
Sign in
Back

textual-reactive-programming

Included with Lifetime
$97 forever

Builds reactive Textual widgets using computed properties, watchers, and reactive attributes. Use when implementing data-binding patterns, responding to state changes, computed properties, efficient updates based on attribute changes, and validation patterns. Covers reactive decorators, watch methods, and avoiding manual refresh patterns.

General

What this skill does


# Textual Reactive Programming

## Purpose
Implement efficient, declarative data binding in Textual widgets using reactive attributes. This pattern eliminates manual refresh calls and ensures UI stays synchronized with state.

## Quick Start

```python
from textual.reactive import reactive
from textual.widgets import Static

class CounterWidget(Static):
    """Widget with reactive counter."""

    count = reactive(0)  # Reactive attribute initialized to 0

    def render(self) -> str:
        """Auto-called when count changes."""
        return f"Count: {self.count}"

    def increment(self) -> None:
        """Increment counter - triggers render()."""
        self.count += 1

# Usage:
widget = CounterWidget()
widget.increment()  # Automatically re-renders
```

## Instructions

### Step 1: Define Reactive Attributes

Declare reactive attributes using `reactive()`:

```python
from textual.reactive import reactive
from textual.widgets import Static
from typing import Literal

class ReactiveWidget(Static):
    """Widget with multiple reactive attributes."""

    # Basic reactive attribute
    status: reactive[str] = reactive("idle", init=False)

    # Reactive with initial value
    count: reactive[int] = reactive(0)

    # Reactive with complex type
    items: reactive[list[str]] = reactive(list, init=False)

    # Reactive with default name (attribute name used)
    message: reactive[str] = reactive("default message")

    # Reactive with type union
    state: reactive[Literal["on", "off"]] = reactive("off")

    def __init__(self, **kwargs: object) -> None:
        super().__init__(**kwargs)
        # Initialize reactive attributes in __init__
        # Only if init=False is set above
        # Otherwise they're initialized by reactive()
```

**Reactive Declaration Options:**
```python
reactive(
    default_value,              # Required: initial value
    init=False,                 # True: initialize in __init__, False: initialize here
    layout=False,               # True: trigger layout when changed
    recompose=False,            # True: call compose() when changed
)
```

**Important Rules:**
- Type hints are required: `attribute: reactive[Type]`
- `init=False` means initialize in `__init__`
- `init=True` (default) means initialize by reactive()
- Changing a reactive attribute triggers a watcher/refresh

### Step 2: Use Watch Methods

Watch methods automatically trigger when reactive attributes change:

```python
from textual.reactive import reactive
from textual.widgets import Static
from typing import Literal

class WatcherWidget(Static):
    """Widget demonstrating watch methods."""

    status: reactive[Literal["idle", "running", "error"]] = reactive("idle", init=False)
    count: reactive[int] = reactive(0)
    items: reactive[list[str]] = reactive(list, init=False)

    def __init__(self, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self.status = "idle"
        self.items = []

    def watch_status(self, old_value: str, new_value: str) -> None:
        """Called when status changes.

        Args:
            old_value: Previous status.
            new_value: New status.
        """
        # Update UI based on status change
        print(f"Status changed: {old_value} -> {new_value}")
        self.refresh()  # Re-render widget

    def watch_count(self, old_value: int, new_value: int) -> None:
        """Called when count changes."""
        if new_value < 0:
            # Revert invalid change
            self.count = old_value
        else:
            # Count is valid
            self.refresh()

    def watch_items(self, old_value: list[str], new_value: list[str]) -> None:
        """Called when items list changes.

        Note: Called on replacement, not on list mutations.
        """
        print(f"Items changed: {len(old_value)} -> {len(new_value)} items")
        self.refresh()

    def add_item(self, item: str) -> None:
        """Add item (triggers watch_items)."""
        # Replace list to trigger watcher
        self.items = self.items + [item]  # Creates new list
        # NOT: self.items.append(item)  # This won't trigger watcher

    def increment(self) -> None:
        """Increment count (triggers watch_count)."""
        self.count += 1  # Triggers watch_count
```

**Watcher Pattern:**
- Method name: `watch_{attribute_name}`
- Signature: `watch_method(self, old_value: Type, new_value: Type) -> None`
- Called automatically when attribute changes
- Called BEFORE re-render
- Use for validation, side effects, cascading updates

### Step 3: Implement Computed Properties

Use computed attributes that derive from other reactive attributes:

```python
from textual.reactive import reactive, var
from textual.widgets import Static

class ComputedWidget(Static):
    """Widget with computed reactive attributes."""

    first_name: reactive[str] = reactive("John", init=False)
    last_name: reactive[str] = reactive("Doe", init=False)

    # Computed property (read-only)
    @property
    def full_name(self) -> str:
        """Derived from first_name and last_name."""
        return f"{self.first_name} {self.last_name}"

    # Alternative: Use var() for derived reactive attribute
    full_name_reactive: reactive[str] = reactive("John Doe")

    def watch_first_name(self, old_value: str, new_value: str) -> None:
        """Update computed attribute when first_name changes."""
        self.full_name_reactive = f"{new_value} {self.last_name}"
        self.refresh()

    def watch_last_name(self, old_value: str, new_value: str) -> None:
        """Update computed attribute when last_name changes."""
        self.full_name_reactive = f"{self.first_name} {new_value}"
        self.refresh()

    def render(self) -> str:
        return f"Full Name: {self.full_name}"
```

**Pattern:**
- Use `@property` for computed values derived from reactive attrs
- Watchers update derived attributes when dependencies change
- Re-render when computed attributes change

### Step 4: Handle Complex State Changes

Manage complex updates with multiple reactive attributes:

```python
from textual.reactive import reactive
from textual.widgets import Static
from dataclasses import dataclass

@dataclass(frozen=True)
class DataPoint:
    """Immutable data point."""
    value: float
    timestamp: str

class ComplexStateWidget(Static):
    """Widget managing complex reactive state."""

    # Multiple reactive attributes
    data: reactive[list[DataPoint]] = reactive(list, init=False)
    total: reactive[float] = reactive(0.0)
    average: reactive[float] = reactive(0.0)
    loading: reactive[bool] = reactive(False)
    error: reactive[str | None] = reactive(None)

    def __init__(self, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self.data = []
        self.total = 0.0
        self.average = 0.0
        self.loading = False
        self.error = None

    def watch_data(self, old_value: list[DataPoint], new_value: list[DataPoint]) -> None:
        """Recalculate totals when data changes."""
        if not new_value:
            self.total = 0.0
            self.average = 0.0
            return

        # Update computed values
        self.total = sum(p.value for p in new_value)
        self.average = self.total / len(new_value)
        self.error = None

    async def load_data(self) -> None:
        """Load data with loading state."""
        try:
            self.loading = True
            self.error = None

            # Simulate async fetch
            import asyncio
            await asyncio.sleep(1)

            # Update data (triggers watch_data)
            self.data = [
                DataPoint(10.5, "2024-01-01"),
                DataPoint(20.3, "2024-01-02"),
                DataPoint(15.8, "2024-01-03"),
            ]
        except Exception as e:
            self.error = str(e)
            self.data = []
        finally:
            self.loading = False

    def render(self) -> str:
        """Render w

Related in General