Lists
Call .map() on a state that holds an array to render one row per item:
import { state } from "qwrk";
function Todos() {
const todos = state([
{ text: "Write docs", done: false },
{ text: "Ship it", done: false },
]);
function add() {
todos.value.push({ text: "Celebrate", done: false });
}
return (
<>
<ul>
{todos.map((todo) => (
<li>{todo.text}</li>
))}
</ul>
<button onClick={add}>Add</button>
</>
);
}The function runs once for each new item, and the list keeps its rows in sync with the array:
push,unshiftandspliceinsert and remove only those rows.sort,reverseand swaps move the existing rows, with their DOM nodes, so inputs keep what the user typed.- Assigning a new array works too:
todos.value = todos.value.filter((todo) => !todo.done)removes the rows of the done items and keeps the others. todos.value = []clears the list at once.
Keys
Rows are keyed by the items themselves: objects by identity, strings and numbers by value. There is no key prop.
A new object is a new item, even if it holds the same data, so its row is rebuilt:
todos.value = todos.value.map((todo) => ({ ...todo })); // rebuilds every row
todos.value[0].done = true; // keeps the row, and notifies todosThe same item twice renders two rows.
Items
The function receives each item raw, as stored: nested states and primitives as they are. It gets no index, since the index changes when rows move. To change an item, write through the state: todos.value[i].done = true still notifies, since reads through the state wrap, but writing a captured item does nothing.
To keep a value in a row up to date, store it in a state inside the item:
const todos = state([{ text: state("Write docs") }]);
<ul>{todos.map((todo) => <li>{todo.text}</li>)}</ul>;
todos.value[0].text.value = "Write more docs"; // updates only that <li>A captured item subscribes to nothing on its own, so read changing fields through the state inside a derive. Read fields that don't change once, in the function:
todos.map((todo) => {
const { id } = todo;
return <li class={derive(() => (selected.value === id ? "active" : ""))}>{id}</li>;
});With the compiler, write the comparison inline. selected.value === todo.id compiles to a keyed check, so changing the selection re-runs only the two rows it affects:
todos.map((todo) => (
<li class={selected.value === todo.id ? "active" : ""}>{todo.id}</li>
));Cleanup
Each row owns the derives and effects its function created. Removing the row stops them, and its DOM is freed once nothing references it. A list created while a derive runs, such as in a component the derive renders, stops with all its rows when that derive runs again.
Filtered and derived lists
.map() works on derives too, so a filtered view keeps the rows of the items that stay:
const open = derive(() => todos.value.filter((todo) => !todo.done));
<ul>{open.map((todo) => <li>{todo.text}</li>)}</ul>;A derive that returns the rows, derive(() => todos.value.map(...)), rebuilds every row on each change. It is fine for a handful of items. With the compiler, {todos.value.map(...)} in JSX behaves the same way.