State
State is the data of a component that can change in the browser. $signal declares a value that you write, and $memo declares a value that is computed from other values.
The sample runs when it comes into view.
$signal
$signal(value) declares writable state. The variable is the value: read it like any variable, and assign to it to change it.
<script>
import { $signal } from 'brumate/signals';
let name = $signal('Ada');
let tags = $signal<string[]>([]);
let selected = $signal<number>();
</script>
- Declare a signal with
let, because you assign to it.constis a compile error. - Assigning a new value updates every part of the page that reads it.
count++,count += 1andcount = count + 1all work. - The initial value is computed once, on the server, when the component renders.
- Without an argument, the value is
undefined. Give the type as$signal<number>(), and the type includesundefined. - The initial value does not track what it reads.
let total = $signal(price * 2)keeps its value whenpricechanges. Use$memofor a value that follows other values.
The sample runs when it comes into view.
Where you can write a signal
A signal can be written in code that runs in the browser:
- Event handlers, such as
onClickandonInput. - Functions that a handler calls.
$effectand$clientcallbacks. See Effects and browser code.
Code that runs while the component renders cannot write a signal. Its value at render time is the value that the page starts with, so a later write there would have no effect. The compiler reports it:
<script>
import { $signal } from 'brumate/signals';
let count = $signal(0);
count = 5;
</script>
Signal "count" can only be written in event handlers, effects, $client callbacks and functions (App.brm:5:3)
Compute the starting value in the call instead: let count = $signal(5).
$memo
$memo(expression) declares a value that is computed from other values. When a value that it reads changes, the memo computes again.
The sample runs when it comes into view.
- Declare a memo with
const.letis a compile error, and so is assigning to the memo:Cannot assign to memo "count". - A memo can read other memos.
countabove readsshown, which readsitemsandquery. - A memo computes on the server for the first render. The browser gets its value with the page, so it does not compute again until a value it reads changes.
- Keep a memo free of side effects, such as writing a signal or changing the page. Use an effect for those.
A memo can also hold a promise, such as the result of a server function. Async data covers that.
$produce
A signal changes when you assign a new value. To change one item of an array or one field of an object, $produce gives you a draft that you can change in place, and writes the result as a new value:
The sample runs when it comes into view.
- The new value shares every object that the recipe did not change. With a keyed
#for, only the changed item updates. - If the recipe changes nothing, the signal keeps its value and nothing updates.
- The recipe can return a new value instead of changing the draft.
- Only plain objects and arrays are drafted. A
Map, aSetor a class instance inside the value is used as it is. $producewrites the signal, so it is allowed where writes are.
Without $produce, write a new value yourself: todos = todos.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo)).
$untrack
$untrack(expression) reads values without depending on them. The memo or effect around it does not compute again when they change.
Here the list follows the filter as you type, but a new page size only applies with the next change of the filter:
The sample runs when it comes into view.
$untrack also covers the functions that the expression calls. Effects shows why that matters.
Rules and common mistakes
- Changing a value in place does not update anything, because the signal still holds the same object.
todos.push(todo)andtodo.done = trueare compile warnings, such asThis changes the value of todos in place, so nothing that reads it updates. Assign a new value, or use$produce. - A signal never leaves its file. A function receives the value of a signal, not the signal, so the compiler can see every read and write. To let another component change a value, pass it a function, such as
onChange=((value) => (count = value)). See Components. - Functions imported from other modules work with values.
format(count)gets a number, and cannot writecount. - Each signal holds one value. There are no stores: an object or array changes by assigning a new value, or with
$produce. - Declare signals and memos at the top level of a
<script>, or of the script of a block. A declaration inside a function is a compile error.
What the browser gets
The server renders the page with the values of every signal and memo. The browser only gets the values that its code reads, such as a signal that a handler writes and the memos that read it. A signal that no code in the browser writes stays on the server, and the page shows it as plain HTML.
What runs where explains which code and values the browser gets.