# lazily
Lazy reactive primitives for Rust — Context, Slots, Cells with automatic dependency tracking and cache invalidation.
[](https://crates.io/crates/lazily)
## Overview
`lazily` provides three core primitives for lazy reactive computation:
- **Context** — owns all reactive state and manages the dependency graph
- **Slot** — a lazily-computed cached value that automatically tracks dependencies
- **Cell** — a mutable value that invalidates dependent Slots when changed
Values are **lazy**: dependents are cleared on invalidation but only recomputed when accessed. This contrasts with eager "signal" systems that recompute immediately.
## Usage
```rust
use lazily::Context;
let ctx = Context::new();
// Create a mutable cell
let counter = ctx.cell(0i32);
// Create a derived slot (automatically tracks dependencies)
val * 2
});
assert_eq!(ctx.get(&doubled), 0);
// Mutate the cell — dependents are cleared (not recomputed yet)
ctx.set_cell(&counter, 5);
// Slot recomputes lazily on next access
assert_eq!(ctx.get(&doubled), 10);
```
## Why Lazy?
| **When does recomputation happen?** | On access (`get`) | Immediately on change |
| **Wasted work** | Zero — only compute what's read | Can compute values nobody uses |
| **Glitch-free** | By construction | Requires topological sorting |
| **Ordering** | Irrelevant — pull-based | Critical — push-based DAG walk |
| **Use case** | Request handling, data pipelines | UI rendering, real-time updates |
In a web server handling requests, you might have 50 computed values available but any given request only uses 5. With eager reactivity, all 50 recompute on every change. With lazy, only the 5 actually accessed compute.
## Core Concepts
### Context
`Context` owns all Slots and Cells. It manages the dependency graph and provides the API for creating, reading, and mutating reactive values. Think of it as the "world" for your reactive computations — in web frameworks, this maps to a request context, application scope, or component tree.
### Slot
A `SlotHandle<T>` wraps a compute function `Fn(&Context) -> T`. The result is cached after first access. Dependencies are discovered automatically via a thread-local tracking stack — any Slot or Cell accessed during computation becomes a dependency.
When a dependency is invalidated, the Slot clears its cached value. It does **not** recompute until `ctx.get()` is called again.
**Dependencies are dynamic.** Every time a Slot recomputes, it re-discovers its dependencies from scratch. If your compute function has conditional branches that access different Cells depending on state, the dependency graph updates automatically. No stale subscriptions, no manual cleanup.
### Cell
A `CellHandle<T>` holds a mutable value. `ctx.set_cell()` compares old and new values via `PartialEq` — if unchanged, no invalidation occurs. If changed, all dependent Slots are recursively cleared.
## API
| `Context::new()` | Create a new context |
| `ctx.slot(\|ctx\| T)` | Create a lazily-computed slot |
| `ctx.get(&slot)` | Get value (computes if unset) |
| `ctx.cell(value)` | Create a mutable cell |
| `ctx.get_cell(&cell)` | Get cell value |
| `ctx.set_cell(&cell, value)` | Update cell (clears dependents if changed) |
| `ctx.is_set(&slot)` | Check if slot has cached value |
| `slot.clear(&ctx)` | Clear cached value and cascade to dependents |
| `cell.clear_dependents(&ctx)` | Clear downstream slots without changing cell value |
## Design
- **Lazy, not eager:** Slots clear on invalidation but only recompute on access
- **PartialEq guard:** `Cell.set()` only invalidates when value actually changes
- **Dynamic dependencies:** Edges re-discovered on each recomputation (no stale subscriptions)
- Interior mutability via `RefCell` (single-threaded)
- Thread-local tracking stack for automatic dependency discovery
- Zero external dependencies
## Multi-Language
lazily is implemented across three languages with shared semantics:
| Context | Owned `Context` struct | Explicit allocator | Plain `dict` |
| Slot creation | `Box<dyn Fn>` closures | `comptime` function pointers | Lambdas |
| Cell equality | `PartialEq` trait | `std.meta.eql` | `!=` operator |
| Thread safety | Single-threaded (`RefCell`) | Mutex by default | GIL |
| Storage | Unified generics | `.direct` / `.indirect` | Object identity |
## Related
- [lazily-zig](https://github.com/btakita/lazily-zig) — Zig implementation with FFI support
- [lazily-py](https://github.com/btakita/lazily-py) — Python implementation with context-as-dict
- [Blog post: Lazily — Reactive Primitives Done Right](https://briantakita.me/posts/lazily-reactive-signals)
## License
MIT