useState
Returns a deep reactive proxy of a plain object or array. Also exported from @realmorg/realm for use outside components.
useState<T extends object>(initial: T): Tconst todo = useState({
items: [],
filter: "all",
get remaining() {
return this.items.filter((item) => !item.done).length;
},
});Parameters
| Parameter | Type | Meaning |
|---|---|---|
initial |
plain object or array | The object to track. It is not copied: the proxy reads and writes it |
Returns the proxy. Write to it directly: todo.filter = "done", todo.items.push(item).
What is tracked
| Operation | Tracked |
|---|---|
| Reading and writing properties | yes, per property |
Adding and deleting keys, in, Object.keys, for…in |
yes |
| Nested plain objects and arrays | yes, wrapped lazily when read |
Array index writes, length, push, pop, shift, unshift, splice, sort, reverse, fill, copyWithin |
yes |
includes, indexOf, lastIndexOf |
yes, and they find both raw and proxied members |
Map, Set, Date, class instances |
no. They are returned unchanged |
Writing the same value (Object.is) does not notify.
Getters and setters
- A getter is a cached derived value. It recomputes only after something it read changes, and only when read again.
- A setter runs with the proxy as
this, so writes inside it are tracked. - A getter that throws reports the error to whoever reads it, and recovers on the next change.
Identity
- The same object always gives the same proxy.
useState(useState(x))returns the proxy unchanged. toRaw(proxy)returns the original object.
Batching
Changes are collected and applied once per microtask, so several writes in a handler cause one update. tick() waits for that update. flush() applies it synchronously.
Example
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>useState demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="./todo-mini.html"></import-element>
<todo-mini></todo-mini>
</body>
</html><custom-element name="todo-mini">
<script>
const todo = useState({
draft: "",
items: [{ id: 1, title: "Read the reference", done: true }],
get remaining() {
return this.items.filter((item) => !item.done).length;
},
});
function add() {
if (!todo.draft.trim()) return;
todo.items.push({ id: Date.now(), title: todo.draft, done: false });
todo.draft = "";
}
</script>
<style>
:host {
display: grid;
gap: calc(var(--spacing) * 2);
width: min(calc(var(--spacing) * 100), 100%);
}
form {
display: flex;
gap: calc(var(--spacing) * 2);
}
label {
display: flex;
gap: calc(var(--spacing) * 2);
}
</style>
<form onsubmit="add(); return false">
<input aria-label="New task" _value="#todo.draft" oninput="todo.draft = this.value">
<ui-button type="submit">Add</ui-button>
</form>
<div>
<label repeat="#todo.items" as="item" key="id">
<input type="checkbox" _checked="#item.done" onchange="item.done = this.checked">
<slot name="#item.title"></slot>
</label>
</div>
<p><slot name="#todo.remaining"></slot> remaining</p>
</custom-element>