You know React. You know props, hooks, and the data flow. Quilt is the same idea scaled up — instead of components, you have cells. And instead of "lift state up", the state lives in the cell itself.
useEffect + useState bundle, encapsulated in a deployable silo.
useStatefunction Counter() {
const [count, setCount] = useState(0);
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>
+
</button>
</div>
);
}
cells:
count:
kind: value
value: 0
message:
kind: formula
formula: '"Count is " + count'
In React, the value lives in component state. In Quilt, it lives in a cell. The cell is the source of truth, addressable by name, and any formula referencing it will update when it changes.
useMemofunction Display({ count }) {
const doubled = useMemo(
() => count * 2, [count]
);
return <div>{doubled}</div>;
}
cells:
count:
kind: value
value: 0
doubled:
kind: formula
formula: "count * 2"
Same semantics — the formula re-evaluates when its dependencies change. But in Quilt, the formula is a cell, not a function call. You can address it from anywhere, subscribe to it, persist it.
useEffect with fetchfunction User({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(setUser);
}, [id]);
if (!user) return <Loading/>;
return <div>{user.name}</div>;
}
cells:
user:
kind: api
url: "https://api.example.com/users/{{id}}"
cache_ttl: 60000
user.name:
kind: formula
formula: "user.name"
The Quilt api cell has caching, error handling, and dependency tracking built in. The same URL with the same parameters is a content-addressed value — you can subscribe to it from anywhere in your app.
| Concept | React | Quilt |
|---|---|---|
| State location | Component tree | A cell, addressable by name |
| Data flow | Props down, callbacks up | Reactive propagation through the cell graph |
| Async data | useEffect + fetch + useState | One api cell |
| Derived state | useMemo + dependencies array | A formula cell — deps are auto-discovered |
| Side effects | useEffect with deps | A listener cell — fires when upstream changes |
| LLM calls | Custom hook with provider abstraction | An ai cell with built-in caching, content addressing |
| Cross-app sharing | Context, Redux, Zustand | Subscribe to any cell by URI, across tiers, across processes |
| Persistence | localStorage, IndexedDB, server DB | FederatedArtifactStore — 3-tier cache + R2 canonical |
| Reactivity | Built into React, but stateful | Pure: cells have values, propagation is deterministic |
In React, you think components. In Quilt, you think cells. The component model assumes a tree; the cell model assumes a graph. The component model assumes render; the cell model assumes propagation. The component model assumes local state; the cell model assumes addressable state.
When you start a Quilt project, you don't reach for a component. You reach for a cell:
// React: a component with state, props, effects
<UserProfile userId={42} onLogout={handleLogout} />
// Quilt: a graph of cells, all addressable
cells:
user:
kind: api
url: "https://api.example.com/users/42"
user.name:
kind: formula
formula: "user.name"
is_logged_in:
kind: formula
formula: "user != null"
on_logout:
kind: listener
depends_on: [is_logged_in]
fn: "if (!is_logged_in) clearSession()"
You don't pass props. You address cells by name. The graph IS the app. The propagation IS the render. The subscription IS the re-render.
Quilt isn't a React replacement. They have different sweet spots:
You can also use both: render Quilt cells as React props.
// In a React component
import { useCell } from '@quilt/react';
function Profile() {
const user = useCell('user');
return <div>{user.name}</div>; // re-renders when the user cell changes
}
(@quilt/react is a thin adapter: useCell = cell => useState + useEffect subscribe.)
Open Quilt Live in your browser — a self-contained 70KB React-free Quilt runtime. Or run npm install @quilt/core and try the patterns above in your own app.