{"id":26417418,"url":"https://github.com/buiapp/reaktiv","last_synced_at":"2026-02-14T15:09:43.902Z","repository":{"id":274886973,"uuid":"924404375","full_name":"buiapp/reaktiv","owner":"buiapp","description":"Signals for Python - inspired by Angular Signals / SolidJS. Reactive Declarative State Management Library for Python - automatic dependency tracking and reactive updates for your application state.","archived":false,"fork":false,"pushed_at":"2026-01-20T22:34:13.000Z","size":3686,"stargazers_count":401,"open_issues_count":2,"forks_count":6,"subscribers_count":5,"default_branch":"main","last_synced_at":"2026-01-21T08:34:25.831Z","etag":null,"topics":["angular","async","asyncio","dependency","python","reactivity","rx","signals","solidjs","state","state-management"],"latest_commit_sha":null,"homepage":"https://reaktiv.bui.app","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/buiapp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":"buiapp","tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":null,"buy_me_a_coffee":null,"thanks_dev":null,"custom":null}},"created_at":"2025-01-29T23:52:30.000Z","updated_at":"2026-01-20T22:34:17.000Z","dependencies_parsed_at":"2025-02-16T13:41:04.382Z","dependency_job_id":"3c1941ae-7bfd-4912-acd2-50a0000b49cb","html_url":"https://github.com/buiapp/reaktiv","commit_stats":null,"previous_names":["buiapp/reaktiv"],"tags_count":37,"template":false,"template_full_name":null,"purl":"pkg:github/buiapp/reaktiv","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buiapp%2Freaktiv","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buiapp%2Freaktiv/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buiapp%2Freaktiv/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buiapp%2Freaktiv/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/buiapp","download_url":"https://codeload.github.com/buiapp/reaktiv/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/buiapp%2Freaktiv/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29447771,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-14T14:10:32.461Z","status":"ssl_error","status_checked_at":"2026-02-14T14:09:49.945Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","async","asyncio","dependency","python","reactivity","rx","signals","solidjs","state","state-management"],"created_at":"2025-03-18T01:01:53.458Z","updated_at":"2026-02-14T15:09:43.891Z","avatar_url":"https://github.com/buiapp.png","language":"Python","funding_links":["https://ko-fi.com/buiapp","https://ko-fi.com/H2H71OBINS"],"categories":["Python","Angular-Inspired Solutions"],"sub_categories":["Wrappers"],"readme":"# reaktiv\n\n\u003cdiv align=\"center\"\u003e\n\n![reaktiv](./docs/assets/logo.png)\n\n![Python Version](https://img.shields.io/badge/python-3.9%2B-blue) [![PyPI Version](https://img.shields.io/pypi/v/reaktiv.svg)](https://pypi.org/project/reaktiv/) [![PyPI Downloads](https://static.pepy.tech/badge/reaktiv/month)](https://pepy.tech/projects/reaktiv) ![Documentation Status](https://readthedocs.org/projects/reaktiv/badge/) ![License](https://img.shields.io/badge/license-MIT-green) [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/H2H71OBINS)\n\n**Reactive Declarative State Management Library for Python** - automatic dependency tracking and reactive updates for your application state.\n\n[Website](https://reaktiv.bui.app/) | [Live Playground](https://reaktiv.bui.app/#playground) | [Documentation](https://reaktiv.bui.app/docs) | [Deep Dive Article](https://bui.app/the-missing-manual-for-signals-state-management-for-python-developers/)\n\n\u003c/div\u003e\n\n## Installation\n\n```bash\npip install reaktiv\n# or with uv\nuv pip install reaktiv\n```\n\n`reaktiv` is a **reactive declarative state management library** that lets you **declare relationships between your data** instead of manually managing updates. When data changes, everything that depends on it updates automatically - eliminating a whole class of bugs where you forget to update dependent state.\n\n**Think of it like Excel spreadsheets for your Python code**: when you change a cell value, all formulas that depend on it automatically recalculate. That's exactly how `reaktiv` works with your application state.\n\n**Key benefits:**\n- 🐛 **Fewer bugs**: No more forgotten state updates or inconsistent data\n- 📋 **Clearer code**: State relationships are explicit and centralized\n- ⚡ **Better performance**: Only recalculates what actually changed (fine-grained reactivity)\n- 🔄 **Automatic updates**: Dependencies are tracked and updated automatically\n- 🎯 **Python-native**: Built for Python's patterns with full async support\n- 🔒 **Type safe**: Full type hint support with automatic inference\n- 🚀 **Lazy evaluation**: Computed values are only calculated when needed\n- 💾 **Smart memoization**: Results are cached and only recalculated when dependencies change\n\n## Documentation\n\nFull documentation is available at [https://reaktiv.bui.app/docs/](https://reaktiv.bui.app/docs/).\n\nFor a comprehensive guide, check out [The Missing Manual for Signals: State Management for Python Developers](https://bui.app/the-missing-manual-for-signals-state-management-for-python-developers/).\n\n## Quick Start\n\n```python\nfrom reaktiv import Signal, Computed, Effect\n\n# Your reactive data sources\nname = Signal(\"Alice\")\nage = Signal(30)\n\n# Reactive derived data - automatically stays in sync\n@Computed\ndef greeting():\n    return f\"Hello, {name()}! You are {age()} years old.\"\n\n# Reactive side effects - automatically run when data changes\n# IMPORTANT: Must assign to variable to prevent garbage collection\ngreeting_effect = Effect(lambda: print(f\"Updated: {greeting()}\"))\n\n# Just change your base data - everything reacts automatically\nname.set(\"Bob\")  # Prints: \"Updated: Hello, Bob! You are 30 years old.\"\nage.set(31)      # Prints: \"Updated: Hello, Bob! You are 31 years old.\"\n```\n\n## Core Concepts\n\n`reaktiv` provides three simple building blocks for reactive programming - just like Excel has cells and formulas:\n\n1. **Signal**: Holds a reactive value that can change (like an Excel cell with a value)\n2. **Computed**: Automatically derives a reactive value from other signals/computed values (like an Excel formula)\n3. **Effect**: Runs reactive side effects when signals/computed values change (like Excel charts that update when data changes)\n\n```python\n# Signal: wraps a reactive value (like Excel cell A1 = 5)\ncounter = Signal(0)\n\n# Computed: derives from other reactive values (like Excel cell B1 = A1 * 2)\n@Computed\ndef doubled():\n    return counter() * 2\n\n# Effect: reactive side effects (like Excel chart that updates when cells change)\ndef print_values():\n    print(f\"Counter: {counter()}, Doubled: {doubled()}\")\n\ncounter_effect = Effect(print_values)\n\ncounter.set(5)  # Reactive update: prints \"Counter: 5, Doubled: 10\"\n```\n\n### Excel Spreadsheet Analogy\n\nIf you've ever used Excel, you already understand reactive programming:\n\n| Cell | Value/Formula | reaktiv Equivalent |\n|------|---------------|-------------------|\n| A1   | `5`           | `Signal(5)`       |\n| B1   | `=A1 * 2`     | `Computed(lambda: a1() * 2)` |\n| C1   | `=A1 + B1`    | `Computed(lambda: a1() + b1())` |\n\nWhen you change A1 in Excel, B1 and C1 automatically recalculate. That's exactly what happens with reaktiv:\n\n```python\n# Excel-style reactive programming in Python\na1 = Signal(5)  # A1 = 5\n\n@Computed  # B1 = A1 * 2\ndef b1() -\u003e int:\n    return a1() * 2\n\n@Computed  # C1 = A1 + B1\ndef c1() -\u003e int:\n    return a1() + b1()\n\n# Display effect (like Excel showing the values)\ndisplay_effect = Effect(lambda: print(f\"A1={a1()}, B1={b1()}, C1={c1()}\"))\n\na1.set(10)  # Change A1 - everything recalculates automatically!\n# Prints: A1=10, B1=20, C1=30\n```\n\nJust like in Excel, you don't need to manually update B1 and C1 when A1 changes - the dependency tracking handles it automatically.\n\n```mermaid\ngraph TD\n    %% Define node subgraphs for better organization\n    subgraph \"Data Sources\"\n        S1[Signal A]\n        S2[Signal B]\n        S3[Signal C]\n    end\n    \n    subgraph \"Derived Values\"\n        C1[Computed X]\n        C2[Computed Y]\n    end\n    \n    subgraph \"Side Effects\"\n        E1[Effect 1]\n        E2[Effect 2]\n    end\n    \n    subgraph \"External Systems\"\n        EXT1[UI Update]\n        EXT2[API Call]\n        EXT3[Database Write]\n    end\n    \n    %% Define relationships between nodes\n    S1 --\u003e|\"get()\"| C1\n    S2 --\u003e|\"get()\"| C1\n    S2 --\u003e|\"get()\"| C2\n    S3 --\u003e|\"get()\"| C2\n    \n    C1 --\u003e|\"get()\"| E1\n    C2 --\u003e|\"get()\"| E1\n    S3 --\u003e|\"get()\"| E2\n    C2 --\u003e|\"get()\"| E2\n    \n    E1 --\u003e EXT1\n    E1 --\u003e EXT2\n    E2 --\u003e EXT3\n    \n    %% Change propagation path\n    S1 -.-\u003e |\"1\\. set()\"| C1\n    C1 -.-\u003e|\"2\\. recompute\"| E1\n    E1 -.-\u003e|\"3\\. execute\"| EXT1\n    \n    %% Style nodes by type\n    classDef signal fill:#4CAF50,color:white,stroke:#388E3C,stroke-width:1px\n    classDef computed fill:#2196F3,color:white,stroke:#1976D2,stroke-width:1px\n    classDef effect fill:#FF9800,color:white,stroke:#F57C00,stroke-width:1px\n    \n    %% Apply styles to nodes\n    class S1,S2,S3 signal\n    class C1,C2 computed\n    class E1,E2 effect\n    \n    %% Legend node\n    LEGEND[\" Legend:\n    • Signal: Stores a value, notifies dependents\n    • Computed: Derives value from dependencies\n    • Effect: Runs side effects when dependencies change\n    • → Data flow / Dependency (read)\n    • ⟿ Change propagation (update)\n    \"]\n    classDef legend fill:none,stroke:none,text-align:left\n    class LEGEND legend\n```\n\n### Additional Features That reaktiv Provides\n\n**Lazy Evaluation** - Computations only happen when results are actually needed:\n```python\n# This expensive computation isn't calculated until you access it\n@Computed\ndef expensive_calc():\n    return sum(range(1000000))  # Not calculated yet!\n\nprint(expensive_calc())  # NOW it calculates when you need the result\nprint(expensive_calc())  # Instant! (cached result)\n```\n\n**Memoization** - Results are cached until dependencies change:\n```python\n# Results are automatically cached for efficiency\na1 = Signal(5)\n\n@Computed\ndef b1():\n    return a1() * 2  # Define the computation\n\nresult1 = b1()  # Calculates: 5 * 2 = 10\nresult2 = b1()  # Cached! No recalculation needed\n\na1.set(6)       # Dependency changed - cache invalidated\nresult3 = b1()  # Recalculates: 6 * 2 = 12\n```\n\n**Fine-Grained Reactivity** - Only affected computations recalculate:\n```python\n# Independent data sources don't affect each other\na1 = Signal(5)    # Independent signal\nd2 = Signal(100)  # Another independent signal\n\n@Computed  # Depends only on a1\ndef b1():\n    return a1() * 2\n\n@Computed  # Depends on a1 and b1\ndef c1():\n    return a1() + b1()\n\n@Computed  # Depends only on d2\ndef e2():\n    return d2() / 10\n\na1.set(10)  # Only b1 and c1 recalculate, e2 stays cached\nd2.set(200) # Only e2 recalculates, b1 and c1 stay cached\n```\n\nThis intelligent updating means your application only recalculates what actually needs to be updated, making it highly efficient.\n\n## The Problem This Solves\n\nConsider a simple order calculation:\n\n### Without reaktiv (Manual Updates)\n```python\nclass Order:\n    def __init__(self):\n        self.price = 100.0\n        self.quantity = 2\n        self.tax_rate = 0.1\n        self._update_totals()  # Must remember to call this\n    \n    def set_price(self, price):\n        self.price = price\n        self._update_totals()  # Must remember to call this\n    \n    def set_quantity(self, quantity):\n        self.quantity = quantity\n        self._update_totals()  # Must remember to call this\n    \n    def _update_totals(self):\n        # Must update in the correct order\n        self.subtotal = self.price * self.quantity\n        self.tax = self.subtotal * self.tax_rate\n        self.total = self.subtotal + self.tax\n        # Oops, forgot to update the display!\n```\n\n### With reaktiv (Excel-style Automatic Updates)\nThis is like Excel - change a cell and everything recalculates automatically:\n\n```python\nfrom reaktiv import Signal, Computed, Effect\n\n# Base values (like Excel input cells)\nprice = Signal(100.0)      # A1\nquantity = Signal(2)       # A2  \ntax_rate = Signal(0.1)     # A3\n\n# Formulas (like Excel computed cells)\n@Computed  # B1 = A1 * A2\ndef subtotal():\n    return price() * quantity()\n\n@Computed  # B2 = B1 * A3\ndef tax():\n    return subtotal() * tax_rate()\n\n@Computed  # B3 = B1 + B2\ndef total():\n    return subtotal() + tax()\n\n# Auto-display (like Excel chart that updates automatically)\ntotal_effect = Effect(lambda: print(f\"Order total: ${total():.2f}\"))\n\n# Just change the input - everything recalculates like Excel!\nprice.set(120.0)  # Change A1 - B1, B2, B3 all update automatically\nquantity.set(3)      # Same thing\n```\n\nBenefits:\n- ✅ Cannot forget to update dependent data\n- ✅ Updates always happen in the correct order\n- ✅ State relationships are explicit and centralized\n- ✅ Side effects are guaranteed to run\n\n## Type Safety \u0026 Decorator Benefits\n\n`reaktiv` provides full type hint support, making it compatible with static type checkers like ruff, mypy and pyright. This enables better IDE autocompletion, early error detection, and improved code maintainability.\n\n```python\nfrom reaktiv import Signal, Computed, Effect\n\n# Explicit type annotations\nname: Signal[str] = Signal(\"Alice\")\nage: Signal[int] = Signal(30)\nactive: Signal[bool] = Signal(True)\n\n# Type inference works automatically\nscore = Signal(100.0)  # Inferred as Signal[float]\nitems = Signal([1, 2, 3])  # Inferred as Signal[list[int]]\n\n# Computed values preserve and infer types\n@Computed\ndef name_length():  # Type is automatically ComputeSignal[int]\n    return len(name())\n\n@Computed\ndef greeting():  # Type is automatically ComputeSignal[str]\n    return f\"Hello, {name()}!\"\n\n@Computed\ndef total_score():  # Type is automatically ComputeSignal[float]\n    return score() * 1.5\n\n# Type-safe update functions\ndef increment_age(current: int) -\u003e int:\n    return current + 1\n\nage.update(increment_age)  # Type checked!\n```\n\n## Why This Pattern?\n\n```mermaid\ngraph TD\n    subgraph \"Traditional Approach\"\n        T1[Manual Updates]\n        T2[Scattered Logic] \n        T3[Easy to Forget]\n        T4[Hard to Debug]\n        \n        T1 --\u003e T2\n        T2 --\u003e T3\n        T3 --\u003e T4\n    end\n    \n    subgraph \"Reactive Approach\"\n        R1[Declare Relationships]\n        R2[Automatic Updates]\n        R3[Centralized Logic]\n        R4[Guaranteed Consistency]\n        \n        R1 --\u003e R2\n        R2 --\u003e R3\n        R3 --\u003e R4\n    end\n    \n    classDef traditional fill:#f44336,color:white\n    classDef reactive fill:#4CAF50,color:white\n    \n    class T1,T2,T3,T4 traditional\n    class R1,R2,R3,R4 reactive\n```\n\nThis reactive approach comes from frontend frameworks like **Angular** and **SolidJS**, where fine-grained reactivity revolutionized UI development. While those frameworks use this reactive pattern to efficiently update user interfaces, the core insight applies everywhere: **declaring reactive relationships between data leads to fewer bugs** than manually managing updates.\n\nThe reactive pattern is particularly valuable in Python applications for:\n- Configuration management with cascading overrides\n- Caching with automatic invalidation\n- Real-time data processing pipelines\n- Request/response processing with derived context\n- Monitoring and alerting systems\n\n## Practical Examples\n\n### Reactive Configuration Management\n```python\nfrom reaktiv import Signal, Computed\n\n# Multiple reactive config sources\ndefaults = Signal({\"timeout\": 30, \"retries\": 3})\nuser_prefs = Signal({\"timeout\": 60})\nfeature_flags = Signal({\"new_retry_logic\": True})\n\n# Automatically reactive merged config\n@Computed\ndef config():\n    return {\n        **defaults(),\n        **user_prefs(),\n        **feature_flags()\n    }\n\nprint(config())  # {'timeout': 60, 'retries': 3, 'new_retry_logic': True}\n\n# Change any source - merged config reacts automatically\ndefaults.update(lambda d: {**d, \"max_connections\": 100})\nprint(config())  # Now includes max_connections\n```\n\n### Reactive Data Processing Pipeline\n```python\nimport time\nfrom reaktiv import Signal, Computed, Effect\n\n# Reactive raw data stream\nraw_data = Signal([])\n\n# Reactive processing pipeline\n@Computed\ndef filtered_data():\n    return [x for x in raw_data() if x \u003e 0]\n\n@Computed\ndef processed_data():\n    return [x * 2 for x in filtered_data()]\n\n@Computed\ndef summary():\n    data = processed_data()\n    return {\n        \"count\": len(data),\n        \"sum\": sum(data),\n        \"avg\": sum(data) / len(data) if data else 0\n    }\n\n# Reactive monitoring - MUST assign to variable!\nsummary_effect = Effect(lambda: print(f\"Summary: {summary()}\"))\n\n# Add data - entire reactive pipeline recalculates automatically\nraw_data.set([1, -2, 3, 4])  # Prints summary\nraw_data.update(lambda d: d + [5, 6])  # Updates summary\n```\n\n#### Reactive Pipeline Visualization\n\n```mermaid\ngraph LR\n    subgraph \"Reactive Data Processing Pipeline\"\n        RD[raw_data\u003cbr/\u003eSignal\u0026lt;list\u0026gt;]\n        FD[filtered_data\u003cbr/\u003eComputed\u0026lt;list\u0026gt;]\n        PD[processed_data\u003cbr/\u003eComputed\u0026lt;list\u0026gt;]\n        SUM[summary\u003cbr/\u003eComputed\u0026lt;dict\u0026gt;]\n        \n        RD --\u003e|reactive filter x \u003e 0| FD\n        FD --\u003e|reactive map x * 2| PD\n        PD --\u003e|reactive aggregate| SUM\n        \n        SUM --\u003e EFF[Effect: print summary]\n    end\n    \n    NEW[New Data] -.-\u003e|\"raw_data.set()\"| RD\n    RD -.-\u003e|reactive update| FD\n    FD -.-\u003e|reactive update| PD\n    PD -.-\u003e|reactive update| SUM\n    SUM -.-\u003e|reactive trigger| EFF\n    \n    classDef signal fill:#4CAF50,color:white\n    classDef computed fill:#2196F3,color:white\n    classDef effect fill:#FF9800,color:white\n    classDef input fill:#9C27B0,color:white\n    \n    class RD signal\n    class FD,PD,SUM computed\n    class EFF effect\n    class NEW input\n```\n\n### Reactive System Monitoring\n```python\nfrom reaktiv import Signal, Computed, Effect\n\n# Reactive system metrics\ncpu_usage = Signal(20)\nmemory_usage = Signal(60)\ndisk_usage = Signal(80)\n\n# Reactive health calculation\nsystem_health = Computed(lambda: \n    \"critical\" if any(x \u003e 90 for x in [cpu_usage(), memory_usage(), disk_usage()]) else\n    \"warning\" if any(x \u003e 75 for x in [cpu_usage(), memory_usage(), disk_usage()]) else\n    \"healthy\"\n)\n\n# Reactive automatic alerting - MUST assign to variable!\nalert_effect = Effect(lambda: print(f\"System status: {system_health()}\") \n                     if system_health() != \"healthy\" else None)\n\ncpu_usage.set(95)  # Reactive system automatically prints: \"System status: critical\"\n```\n\n## Advanced Features\n\n### LinkedSignal (Writable derived state)\n\n`LinkedSignal` is a writable computed signal that can be manually set by users but will automatically reset when its source context changes. Use it for “user overrides with sane defaults” that should survive some changes but reset on others.\n\nCommon use cases:\n- Pagination: selection resets when page changes\n- Wizard flows: step-specific state resets when the step changes\n- Filters \u0026 search: user-picked value persists across pagination, resets when query changes\n- Forms: default values computed from context but user can override temporarily\n\n**Using the @Linked decorator:**\n\n```python\nfrom reaktiv import Signal, Linked\n\npage = Signal(1)\n\n# Writable derived state that resets whenever page changes\n@Linked\ndef selection() -\u003e str:\n    return f\"default-for-page-{page()}\"\n\nselection.set(\"custom-choice\")   # user override\nprint(selection())                # \"custom-choice\"\n\npage.set(2)                       # context changes → resets\nprint(selection())                # \"default-for-page-2\"\n```\n\n**Alternative: factory function style:**\n```python\n# Still supported\nselection = LinkedSignal(lambda: f\"default-for-page-{page()}\")\n```\n\nAdvanced pattern (explicit source and previous-state aware computation):\n\n```python\nfrom reaktiv import Signal, LinkedSignal, PreviousState\n\n# Source contains (query, page). We want selection to persist across page changes\n# but reset when the query string changes.\nquery = Signal(\"shoes\")\npage = Signal(1)\n\ndef compute_selection(src: tuple[str, int], prev: PreviousState[str] | None) -\u003e str:\n    current_query, _ = src\n    # If only the page changed, keep previous selection\n    if prev is not None and isinstance(prev.source, tuple) and prev.source[0] == current_query:\n        return prev.value\n    # Otherwise, provide a new default for the new query\n    return f\"default-for-{current_query}\"\n\nselection = LinkedSignal(source=lambda: (query(), page()), computation=compute_selection)\n\nprint(selection())  # \"default-for-shoes\"\nselection.set(\"red-sneakers\")\n\npage.set(2)         # page changed, same query → keep user override\nprint(selection())  # \"red-sneakers\"\n\nquery.set(\"boots\")  # query changed → reset to new default\nprint(selection())  # \"default-for-boots\"\n```\n\nNotes:\n- It’s writable: call `selection.set(...)` or `selection.update(...)` to override.\n- It auto-resets based on the dependencies you read (simple pattern) or your custom `source` logic (advanced pattern).\n\n### Resource - Async Data Loading\n\n`Resource` brings async operations into your reactive application with automatic dependency tracking, request cancellation, and comprehensive status management. Perfect for API calls and data fetching.\n\n```python\nimport asyncio\nfrom reaktiv import Resource, Signal, ResourceStatus\n\n# Reactive parameter\nuser_id = Signal(1)\n\n# Async data loader\nasync def fetch_user(params):\n    # Check for cancellation\n    if params.cancellation.is_set():\n        return None\n    \n    await asyncio.sleep(0.5)  # Simulate API call\n    return {\"id\": params.params[\"user_id\"], \"name\": f\"User {params.params['user_id']}\"}\n\nasync def main():\n    # Create resource\n    user_resource = Resource(\n        params=lambda: {\"user_id\": user_id()},  # When user_id changes, auto-reload\n        loader=fetch_user\n    )\n    \n    # Wait for initial load\n    await asyncio.sleep(0.6)\n    \n    # Access data safely\n    if user_resource.has_value():\n        print(user_resource.value())  # {\"id\": 1, \"name\": \"User 1\"}\n    \n    # Changing param automatically triggers reload\n    user_id.set(2)\n    await asyncio.sleep(0.6)\n    print(user_resource.value())  # {\"id\": 2, \"name\": \"User 2\"}\n\nasyncio.run(main())\n```\n\n**Key features:**\n- **6 status states**: IDLE, LOADING, RELOADING, RESOLVED, ERROR, LOCAL\n- **Automatic request cancellation** when parameters change (prevents race conditions)\n- **Seamless integration** with Computed and Effect\n- **Manual control** via `reload()`, `set()`, and `update()` methods\n- **Atomic snapshots** for safe state access\n- **Automatic cleanup** when garbage collected\n\n\nFor complete documentation, examples, and design patterns, see the [Resource User Guide](https://reaktiv.bui.app/docs/resource-guide.html).\n\n### Custom Equality\n```python\n# For objects where you want value-based comparison\nitems = Signal([1, 2, 3], equal=lambda a, b: a == b)\nitems.set([1, 2, 3])  # Won't trigger updates (same values)\n```\n\n### Update Functions\n```python\ncounter = Signal(0)\ncounter.update(lambda x: x + 1)  # Increment based on current value\n```\n\n### Async Effects\n\n**Recommendation: Use synchronous effects** - they provide better control and predictable behavior:\n\n```python\nimport asyncio\n\nmy_signal = Signal(\"initial\")\n\n# ✅ RECOMMENDED: Synchronous effect with async task spawning\ndef sync_effect():\n    # Signal values captured at this moment - guaranteed consistency\n    current_value = my_signal()\n    \n    # Spawn async task if needed\n    async def background_work():\n        await asyncio.sleep(0.1)\n        print(f\"Processing: {current_value}\")\n    \n    asyncio.create_task(background_work())\n\n# MUST assign to variable!\nmy_effect = Effect(sync_effect)\n```\n\n**Experimental: Direct async effects**\n\nAsync effects are experimental and should be used with caution:\n\n```python\nimport asyncio\n\nasync def async_effect():\n    await asyncio.sleep(0.1)\n    print(f\"Async processing: {my_signal()}\")\n\n# MUST assign to variable!\nmy_async_effect = Effect(async_effect)\n```\n\n**Key differences:**\n- **Synchronous effects**: Block the signal update until complete, ensuring signal values don't change during effect execution\n- **Async effects** (experimental): Allow signal updates to complete immediately, but signal values may change while the async effect is running\n\n**Note:** Most applications should use synchronous effects for predictable behavior.\n\n### Untracked Reads\nUse `untracked()` to read signals without creating dependencies:\n\n```python\nfrom reaktiv import Signal, Computed, Effect\n\nuser_id = Signal(1)\ndebug_mode = Signal(False)\n\n# This computed only depends on user_id, not debug_mode\ndef get_user_data():\n    uid = user_id()  # Creates dependency\n    if untracked(debug_mode):  # No dependency created\n        print(f\"Loading user {uid}\")\n    return f\"User data for {uid}\"\n\nuser_data = Computed(get_user_data)\n\ndebug_mode.set(True)   # Won't trigger recomputation\nuser_id.set(2)         # Will trigger recomputation\n```\n\n**Context Manager Usage**\n\nYou can also use `untracked` as a context manager to read multiple signals without creating dependencies. This is useful for logging or conditional logic inside an effect without adding extra dependencies.\n\n```python\nfrom reaktiv import Signal, Computed, Effect, untracked\n\nname = Signal(\"Alice\")\nis_logging_enabled = Signal(False)\nlog_level = Signal(\"INFO\")\n\ngreeting = Computed(lambda: f\"Hello, {name()}!\")\n\n# An effect that depends on `greeting`, but reads other signals untracked\ndef display_greeting():\n    # Create a dependency on `greeting`\n    current_greeting = greeting()\n    \n    # Read multiple signals without creating dependencies\n    with untracked():\n        logging_active = is_logging_enabled()\n        current_log_level = log_level()\n        if logging_active:\n            print(f\"LOG [{current_log_level}]: Greeting updated to '{current_greeting}'\")\n    \n    print(current_greeting)\n\n# MUST assign to variable!\ngreeting_effect = Effect(display_greeting)\n# Initial run prints: \"Hello, Alice\"\n\nname.set(\"Bob\")\n# Prints: \"Hello, Bob\"\n\nis_logging_enabled.set(True)\nlog_level.set(\"DEBUG\")\n# Prints nothing, because these are not dependencies of the effect.\n\nname.set(\"Charlie\")\n# Prints:\n# LOG [DEBUG]: Greeting updated to 'Hello, Charlie'\n# Hello, Charlie\n```\n\nThe context manager approach is particularly useful when you need to read multiple signals for logging, debugging, or conditional logic without creating reactive dependencies.\n\n### Batch Updates\nUse `batch()` to group multiple updates and trigger effects only once:\n\n```python\nfrom reaktiv import Signal, Effect, batch\n\nname = Signal(\"Alice\")\nage = Signal(30)\ncity = Signal(\"New York\")\n\ndef print_info():\n    print(f\"{name()}, {age()}, {city()}\")\n\ninfo_effect = Effect(print_info)\n# Effect prints one time on init\n\n# Without batch - prints 3 times\nname.set(\"Bob\")\nage.set(25)\ncity.set(\"Boston\")\n\n# With batch - prints only once at the end\nwith batch():\n    name.set(\"Charlie\")\n    age.set(35)\n    city.set(\"Chicago\")\n# Only prints once: \"Charlie, 35, Chicago\"\n```\n\n### Error Handling\n\nProper error handling is crucial to prevent cascading failures:\n\n```python\nfrom reaktiv import Signal, Computed, Effect\n\n# Example: Division computation that can fail\nnumerator = Signal(10)\ndenominator = Signal(2)\n\n# Unsafe computation - can throw ZeroDivisionError\nunsafe_division = Computed(lambda: numerator() / denominator())\n\n# Safe computation with error handling\ndef safe_divide():\n    try:\n        return numerator() / denominator()\n    except ZeroDivisionError:\n        return float('inf')  # or return 0, or handle as needed\n\nsafe_division = Computed(safe_divide)\n\n# Error handling in effects\ndef safe_print():\n    try:\n        unsafe_result = unsafe_division()\n        print(f\"Unsafe result: {unsafe_result}\")\n    except ZeroDivisionError:\n        print(\"Error: Division by zero!\")\n    \n    safe_result = safe_division()\n    print(f\"Safe result: {safe_result}\")\n\neffect = Effect(safe_print)\n\n# Test error scenarios\ndenominator.set(0)  # Triggers ZeroDivisionError in unsafe computation\n# Prints: \"Error: Division by zero!\" and \"Safe result: inf\"\n```\n\n## Important Notes\n\n### ⚠️ Effect Retention (Critical!)\n**Effects must be assigned to a variable to prevent garbage collection.** This is the most common mistake when using reaktiv:\n\n```python\n# ❌ WRONG - effect gets garbage collected immediately and won't work\nEffect(lambda: print(\"This will never print\"))\n\n# ✅ CORRECT - effect stays active\nmy_effect = Effect(lambda: print(\"This works!\"))\n\n# ✅ Also correct - store in a list or class attribute\neffects = []\neffects.append(Effect(lambda: print(\"This also works!\")))\n\n# ✅ In classes, assign to self\nclass MyClass:\n    def __init__(self):\n        self.counter = Signal(0)\n        # Keep effect alive by assigning to instance\n        self.effect = Effect(lambda: print(f\"Counter: {self.counter()}\"))\n```\n\n**Why this design?** This explicit retention requirement prevents accidental memory leaks. Unlike some reactive systems that automatically keep effects alive indefinitely, `reaktiv` requires you to explicitly manage effect lifetimes. When you no longer need an effect, simply let the variable go out of scope or delete it - the effect will be automatically cleaned up. This gives you control over when reactive behavior starts and stops, preventing long-lived applications from accumulating abandoned effects.\n\n**Manual cleanup:** You can also explicitly dispose of effects when you're done with them:\n\n```python\nmy_effect = Effect(lambda: print(\"This will run\"))\n# ... some time later ...\nmy_effect.dispose()  # Manually clean up the effect\n# Effect will no longer run when dependencies change\n```\n\n### Mutable Objects\nBy default, reaktiv uses identity comparison. For mutable objects:\n\n```python\ndata = Signal([1, 2, 3])\n\n# This triggers update (new list object)\ndata.set([1, 2, 3])  \n\n# This doesn't trigger update (same object, modified in place)\ncurrent = data()\ncurrent.append(4)  # reaktiv doesn't see this change\n```\n\n### Working with Lists and Dictionaries\n\nWhen working with mutable objects like lists and dictionaries, you need to create new objects to trigger updates:\n\n#### Lists\n```python\nitems = Signal([1, 2, 3])\n\n# ❌ WRONG - modifies in place, no update triggered\ncurrent = items()\ncurrent.append(4)  # reaktiv doesn't detect this\n\n# ✅ CORRECT - create new list\nitems.set([*items(), 4])  # or items.set(items() + [4])\n\n# ✅ CORRECT - using update() method\nitems.update(lambda current: current + [4])\nitems.update(lambda current: [*current, 4])\n\n# Other list operations\nitems.update(lambda lst: [x for x in lst if x \u003e 2])  # Filter\nitems.update(lambda lst: [x * 2 for x in lst])       # Map\nitems.update(lambda lst: lst[:-1])                   # Remove last\nitems.update(lambda lst: [0] + lst)                  # Prepend\n```\n\n#### Dictionaries\n```python\nconfig = Signal({\"timeout\": 30, \"retries\": 3})\n\n# ❌ WRONG - modifies in place, no update triggered\ncurrent = config()\ncurrent[\"new_key\"] = \"value\"  # reaktiv doesn't detect this\n\n# ✅ CORRECT - create new dictionary\nconfig.set({**config(), \"new_key\": \"value\"})\n\n# ✅ CORRECT - using update() method\nconfig.update(lambda current: {**current, \"new_key\": \"value\"})\n\n# Other dictionary operations\nconfig.update(lambda d: {**d, \"timeout\": 60})           # Update value\nconfig.update(lambda d: {k: v for k, v in d.items() if k != \"retries\"})  # Remove key\nconfig.update(lambda d: {**d, **{\"max_conn\": 100, \"pool_size\": 5}})      # Merge multiple\n```\n\n#### Alternative: Value-Based Equality\nIf you prefer to modify objects in place, provide a custom equality function:\n\n```python\n# For lists - compares actual values\ndef list_equal(a, b):\n    return len(a) == len(b) and all(x == y for x, y in zip(a, b))\n\nitems = Signal([1, 2, 3], equal=list_equal)\n\n# Now you can modify in place and trigger updates manually\ncurrent = items()\ncurrent.append(4)\nitems.set(current)  # Triggers update because values changed\n\n# For dictionaries - compares actual content\ndef dict_equal(a, b):\n    return a == b\n\nconfig = Signal({\"timeout\": 30}, equal=dict_equal)\n\ncurrent = config()\ncurrent[\"retries\"] = 3\nconfig.set(current)  # Triggers update\n```\n\n## More Examples\n\nYou can find more example scripts in the [examples](./examples) folder to help you get started with using this project.\n\nIncluding integration examples with:\n\n- [FastAPI - Websocket](./examples/fastapi_websocket.py)\n- [NiceGUI - Todo-App](./examples/nicegui_todo_app.py)\n- [Reactive Data Pipeline with NumPy and Pandas](./examples/data_pipeline_numpy_pandas.py)\n- [Jupyter Notebook - Reactive IPyWidgets](./examples/reactive_jupyter_notebook.ipynb)\n- [NumPy Matplotlib - Reactive Plotting](./examples/numpy_plotting.py)\n- [IoT Sensor Agent Thread - Reactive Hardware](./examples/iot_sensor_agent_thread.py)\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=buiapp/reaktiv\u0026type=Date)](https://star-history.com/#buiapp/reaktiv\u0026Date)\n\n---\n\n**Inspired by** Angular Signals and SolidJS reactivity • **Built for** Python developers who want fewer state management bugs • **Made in** Hamburg\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbuiapp%2Freaktiv","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbuiapp%2Freaktiv","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbuiapp%2Freaktiv/lists"}