Components & JSX
Components run once
A component is a plain function that returns DOM nodes. It runs once, when you call it. There is no re-render: reactivity comes from the state you use in JSX, which keeps that exact text or attribute in sync.
import { state } from "qwrk";
function Counter() {
const count = state(0);
return (
<p>
count: {count}, doubled: {count.value * 2}
</p>
);
}After count.value = 5, the paragraph shows count: 5, doubled: 10. {count} passes the state itself, which always stays live. With the compiler, any expression that reads .value, like {count.value * 2}, stays live too. Without it, such an expression is evaluated once: it would keep showing 0, so use a derive() there.
Props and children
Components receive their props as one object, with nested JSX in children:
function Card({ title, children }) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}
<Card title="Hello">
<p>Body text</p>
</Card>;Fragments
<>...</> groups elements without a wrapper. A fragment is a native DocumentFragment: appending it moves its children into the parent.
function App() {
return (
<>
<h1>Title</h1>
<p>Text</p>
</>
);
}
document.getElementById("root").append(<App />);Events
Any on* prop whose value is a function becomes an event listener. The event name is lowercased, so onClick listens for click and onDblClick for dblclick.
<button onClick={(event) => console.log(event)}>Click me</button>The compiler delegates bubbling events such as click and input to one listener on the document. See Events.
Attributes
| JSX | DOM |
|---|---|
className="a" | class="a" |
htmlFor="id" | for="id" |
disabled={true} | disabled="" |
hidden={false}, null, undefined | attribute removed |
data-id={7} | data-id="7" |
style="color: red" | style="color: red" |
style also takes an object with camelCase, kebab-case or custom property keys. Numbers get px where CSS needs a unit:
<div style={{ backgroundColor: "red", width: 16, opacity: 0.5, "--gap": "4px" }} />
// style="background-color: red; width: 16px; opacity: 0.5; --gap: 4px;"Pass a state to keep an attribute in sync:
const disabled = state(true);
<button disabled={disabled}>Save</button>;
disabled.value = false; // removes the disabled attributeForm inputs
value, checked and selected are set as properties, so they keep working after the user edits the field:
const text = state("");
<input value={text} onInput={(e) => (text.value = e.currentTarget.value)} />;
text.value = ""; // clears the input, even after typing<select value={choice}> selects the matching <option>.
SVG
SVG tags such as <svg>, <path> and <circle> are created as SVG elements, so inline icons work:
<svg viewBox="0 0 24 24" width="24" height="24">
<circle cx="12" cy="12" r="10" fill="currentColor" />
</svg><a>, <title>, <style> and <script> exist in both HTML and SVG, and are always created as HTML, even inside <svg>.
Children
- Strings and numbers render as text, including
0. false,true,nullandundefinedrender nothing.- Arrays are flattened, so a plain array's
items.map(...)works. - A state renders its value and updates in place. It can hold text, a number, an element, a fragment or an array of them:
const view = state(<p>Loading...</p>);
<div>{view}</div>;
view.value = <strong>Done</strong>; // swaps the element
view.value = [<p>One</p>, <p>Two</p>]; // swaps in both
view.value = null; // clears itFor lists that follow a state, use .map(). For conditions, the compiler makes {show.value && <p />} reactive automatically; without it, use derive().
Limitations
Qwrk keeps its core small, so some things are deliberately not there yet:
- Plain expressions are evaluated once without the compiler.
{show.value && <p />}and{todos.value.map(...)}don't update when the state changes. With the compiler they are reactive; without it, wrap conditions inderive(), and render lists withtodos.map(...). - Effects created outside a derive outlive their component.
effect()and.effect()created in a component that no derive renders keep running until you stop them. Inside a derive, or inside another effect's callback, they stop when that one runs again.
Memory
There's no unmount step. What a derive renders, including the DOM bindings of a list row, is disposed when the derive runs again or when its list drops it, and disposal drops every subscription at once. For example, every <li> removed from a list stops updating, even when it shows a state the whole app shares.
Detached trees are not tracked: dropping nodes without going through a derive or a list leaves their subscriptions until the states they read are themselves freed, so prefer derives and lists for anything that comes and goes.
A node you keep a reference to, such as an element stored in a state, stays alive and keeps updating, so you can put it back in the page later.
effect() and .effect() created outside a derive are the exception: they live until you stop them, and they keep the states they read alive. Those created while a derive runs, such as in a list item, stop when it runs again. Those created inside another effect's callback stop when it runs again.