miqro · @miqro/core · @miqro/parser · @miqro/query · @miqro/jsx · @miqro/jsx-dom · @miqro/jsx-node · @miqro/request · @miqro/runner · @miqro/test · @miqro/test-http
JSX vDOM with hooks. runs server-side and in the browser via runtime adapters.
runtimes: browser → @miqro/jsx-dom · server → @miqro/jsx-node
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "JSX.createElement",
"jsxFragmentFactory": "JSX.Fragment"
}
}
import JSX in every .tsx file:
import JSX from "@miqro/jsx";
function Hello() {
return <p>Hello</p>;
}
on* prop names are lowercased when the name is a known DOM event, so onClick and
onclick both attach a click listener. a name that is not a known DOM event keeps its
casing, and data-on* always keeps casing exactly, which is how you reach a custom element
event like myEvent:
<button onClick={fn} /> // addEventListener("click", fn)
<my-widget onMyCustomThing={fn} /> // addEventListener("MyCustomThing", fn)
<my-widget data-onMyEvent={fn} /> // addEventListener("MyEvent", fn)
handler props must be functions. a string valued onclick is ignored and reported rather
than written as an attribute, so spreading untrusted props cannot inject a handler. on* is
typed as a handler, so a string there is a compile error as well.
a handler is given the event, and event.target is the element it fired on. an inline
handler gets its parameter from context, so reading value off the target needs no
annotation and no null check:
function Form() {
const [name, setName] = useState("");
return <input value={name} onInput={e => setName(e.target.value)} />;
}
the type is RuntimeDispatchedEvent, exported for when you want to write it out. it differs
from RuntimeEvent only in that target is not null: an event is given a target when it is
dispatched, and a handler only ever sees a dispatched one. target.checked is optional,
since most elements do not have it, so a checkbox handler narrows it:
setOn(e.target.checked === true).
the event and its target carry what both runtimes can provide: type, detail, target,
target.value, target.checked, preventDefault(), stopPropagation(). anything that only
a browser has is not on them, because the server has none of it to give -- there are no file
pickers, drags or keystrokes while a page is being rendered to a string. a type that promised
files would be handing back undefined on the server.
reach that data with a cast. the cast is the marker: this handler only runs in a browser.
<input type="file" onChange={e => {
const input = e.target as HTMLInputElement;
upload(input.files);
}} />
<div onDrop={e => handle((e as unknown as DragEvent).dataTransfer)} />
<input onKeyDown={e => { if ((e as unknown as KeyboardEvent).key === "Enter") submit(); }} />
<div onClick={e => (e.target as HTMLElement).classList.add("on")} />
cast inside the handler, not on it. annotating the parameter with a narrower target is rejected, because a function taking a more specific argument is not a function that accepts any event:
// does not compile
<input onChange={(e: RuntimeDispatchedEvent & { target: HTMLInputElement }) => ...} />
a generic target parameter does not help either. a DOM HTMLInputElement is not assignable
to RuntimeHTMLElement -- childNodes and appendChild differ -- so no constraint can
accept both. casting is the route.
undefined, null and false remove an attribute rather than being stringified, so
<img src={undefined} /> emits no src at all. everything else is passed through String().
style is a string, not an object. style={{color: "red"}} renders [object Object].
null, undefined, true and false render nothing, so {cond && <X/>} prints nothing
when cond is false.
numbers do render, and && returns its left operand, so guarding on a number prints the
number instead of rendering nothing:
{items.length && <List/>} // renders "0" when the list is empty
{items.length > 0 && <List/>} // renders nothing
same for NaN. guard on a boolean.
key marks a child as a distinct logical item so its node is not reused for a different one.
it is never written into the markup.
{items.map(item => <li key={item.id}>{item.label}</li>)}
keys currently buy correctness, not speed. without a key, a positional diff will reuse one row's element for a different row and carry its uncontrolled state (input value, focus, scroll position) onto the wrong item. a key stops that.
but reordering a keyed list rebuilds the moved nodes rather than moving them, because the
runtime interface has no insertBefore. so for a pure reorder, keys are slower than no
keys: unkeyed reuses every element and patches it in place, keyed rebuilds them all. use keys
when rows hold state worth protecting, not to speed up reordering.
<div dangerouslySetInnerHTML={html} />
the content is not escaped, so only pass HTML you trust. the older innerHTML spelling still
works but warns. children are ignored when either is set.
createElementNS, so
an <svg> subtree will not render.[object Promise].javascript: or data: URL in href or src is
written as given, the same as React. Values that came from user input have to be checked by
the application -- @miqro/parser's url type takes a urlProtocols allowlist.A component is not re-invoked when its props and children are unchanged, so an update no longer re-runs every descendant. Props are compared shallowly, children by identity.
Three things are never skipped:
container.render(), so an explicit render always renders;import JSX, { useState } from "@miqro/jsx";
function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
import JSX, { useState, useEffect } from "@miqro/jsx";
function Timer() {
const [n, setN] = useState(0);
useEffect(() => {
const t = setTimeout(() => setN(n + 1), 1000);
return () => clearTimeout(t);
}, [n]);
return <p>{n}</p>;
}
effects run by default on both runtimes, including a container you build yourself with
runtime.createContainer() on the server. JSXResponseHandler from @miqro/jsx-node is the
exception: it disables effects, events, ref callbacks and refresh for the SSR pass, and takes
runtime options to turn any of them back on.
import JSX, { createContext, useContext } from "@miqro/jsx";
const ThemeContext = createContext();
function App() {
return <ThemeContext.provider value="dark">
<Child />
</ThemeContext.provider>;
}
function Child() {
const theme = useContext(ThemeContext);
return <p>{theme}</p>;
}
with no provider above it, useContext returns the value passed to createContext, or
undefined if none was passed.
syncs state with URL query string.
import JSX, { useQuery } from "@miqro/jsx";
function Page() {
const [page, setPage] = useQuery("page", "1");
const n = Number(page);
return <button onClick={() => setPage(String(n + 1))}>page {page}</button>;
}
the value is a string, a string[] when the parameter is repeated, or null when it is absent
and no default was given, so convert before doing arithmetic on it. the default is a string
too: useQuery("page", 1) does not typecheck.
import JSX, { usePathname } from "@miqro/jsx";
function Nav() {
const path = usePathname();
return <p>current: {path}</p>;
}
import JSX, { useRef, useEffect } from "@miqro/jsx";
function Input() {
const ref = useRef();
useEffect(() => {
if (ref.current) ref.current.focus();
}, []);
return <input ref={ref} />;
}
useRef() takes no arguments. ref.current is live: it is set before effects run, so an
effect on mount already sees the element.
access the underlying DOM element the component renders into.
import JSX, { useElement } from "@miqro/jsx";
function Component() {
const el = useElement();
// el is the RuntimeHTMLElement
}
Component.asFragment = false;
this needs asFragment = false. a component renders as a fragment by default and so has no
element of its own, and useElement() returns null there.
force re-render.
import JSX, { useRefresh } from "@miqro/jsx";
function Component() {
const refresh = useRefresh();
// refresh() forces re-render
}
import JSX, { useRuntime } from "@miqro/jsx";
function Component() {
const runtime = useRuntime();
// runtime.name === "dom" | "node"
}
client-side routing. Router takes a routes array and a defaultElement, it does not take
Route children. Link takes path, not href.
import JSX, { Router, Link } from "@miqro/jsx";
function App() {
return <nav>
<Link path="/home">Home</Link>
<Link path="/about">About</Link>
<Router
defaultElement={<NotFound />}
routes={[
{ path: "/home", element: <Home /> },
{ path: "/about", element: <About /> }
]}
/>
</nav>;
}
Route is a TypeScript interface, so import it with import type:
import type { Route } from "@miqro/jsx";
Link renders an <a> and sets class to classNameActive (default "active") or
classNameInactive (default "inactive"), appended to className when you pass one. pass
disableHandler to get a plain link that does a full navigation.
same component code runs on both runtimes.
import { createNodeRuntime } from "@miqro/jsx-node";
import { createElement } from "@miqro/jsx";
import { MyComponent } from "./component.js";
const runtime = createNodeRuntime({ url: { pathname: "/", searchParams: new URLSearchParams(), toString: () => "/" } });
const root = runtime.createElement("div");
const container = runtime.createContainer(root);
container.render(createElement(MyComponent, { title: "hello" }));
console.log(root.toString()); // <div><p>hello</p></div>
container.disconnect();
import { createDOMContainer } from "@miqro/jsx-dom";
import { createElement } from "@miqro/jsx";
import { MyComponent } from "./component.js";
const root = document.getElementById("root");
const container = createDOMContainer(root);
container.render(createElement(MyComponent, { title: "hello" }));
a component renders as a fragment by default: its output goes straight into the parent with no
wrapper element. set asFragment = false to give it an element of its own, named after the
function.
function MyComponent() { ... }
MyComponent.asFragment = false; // <mycomponent-component><p>...</p></mycomponent-component>
// default: true
function MyComponent() { ... }
MyComponent.asFragment = false; // required, see below
MyComponent.shadowInit = { mode: "open" }; // open shadow root
MyComponent.shadowInit = true; // closed shadow root
MyComponent.shadowInit = false; // no shadow DOM
// default: false
a shadow root is attached to the component's own element, so this only does something with
asFragment = false. on a fragment component there is no element to attach it to and
shadowInit is ignored.
import { enableDebugLog } from "@miqro/jsx";
enableDebugLog();