@xkdkw/framework
v1.8.0
Published
A small JSX framework with hooks, batched component-level updates, memo, context, portals and render-prop children.
Downloads
1,325
Readme
@xkdkw/framework
A small JSX framework with hooks. Components are plain functions; a fiber reconciler diffs their output against the DOM.
npm install @xkdkw/frameworkJSX needs a compiler. With Vite or esbuild:
esbuild: {
jsxFactory: 'h',
jsxFragment: 'Fragment',
jsxInject: `import { h, Fragment } from '@xkdkw/framework'`,
},Rendering
import { render } from '@xkdkw/framework';
render(<App />, document.getElementById('root')!);render mounts synchronously and runs the first effects before it returns.
Updates
- State updates are batched. Every
setStatein the same task becomes one render in a microtask. - Only the component whose state changed re-renders, together with its children. Parents and siblings are left alone.
- Setting state to the value it already has (
Object.is) does nothing. flushSync(fn)runsfnand renders right away, for code that has to read the DOM straight after an update.- Effects run right after their render is committed. State set inside an effect is batched into the next render, never nested inside the current one.
- An update loop that never settles is stopped after 100 passes, with an error in the console.
- A component that throws while updating logs the error and keeps its previous output. The rest of the app keeps working.
Hooks
| Hook | Notes |
| --- | --- |
| useState(initial) | initial may be a function, called once. The setter keeps the same identity for the component's lifetime. |
| useReducer(reducer, initial, init?) | dispatch keeps the same identity. |
| useEffect(effect, deps?) | No deps means after every render. Cleanups run before the next run and on unmount. |
| useLayoutEffect | Same timing as useEffect, both run right after commit. |
| useMemo(factory, deps) / useCallback(fn, deps) | Deps are compared with Object.is. |
| useRef(initial) | The same box on every render. Pass it as a host element's ref to receive the node. |
| useId() | A stable, unique id for the component. |
| useContext(context) | Reads the nearest Provider above the component. |
memo
const Row = memo(function Row({ label }: { label: string }) {
return <li>{label}</li>;
});A memoised component skips rendering when its props are shallowly equal (key is ignored, and children is compared item by item). Its own state updates and context changes still re-render it and anything under it.
Render-prop children
A function passed as a child is kept as a function, so a component can call it with its own state:
function Toggle({ children }: { children: [(state: { on: boolean }) => any] }) {
const [on, setOn] = useState(false);
return <button onClick={() => setOn(!on)}>{children[0]({ on })}</button>;
}
<Toggle>{({ on }) => (on ? 'On' : 'Off')}</Toggle>;A function that reaches the DOM unresolved renders nothing.
Context
const Theme = createContext('light');
<Theme.Provider value="dark">
<Toolbar />
</Theme.Provider>;Providers can be nested. When a Provider's value changes, the consumers below that Provider re-render, including consumers behind a memo and inside portals. Consumers under other Providers of the same context are left alone. A consumer with no Provider above it gets the context's default value.
Portals
createPortal(<div className="popover">...</div>, document.body);The children render into another container but stay part of the component tree for state, context and cleanup. This is the way out of overflow: hidden and transform ancestors.
DOM props
className→class, andhtmlFor→for.value,checked,selected,indeterminateandmutedare set as properties, so controlled inputs follow state even after the user has typed. A<select>value is applied after its options exist.styletakes a string or an object. Removed keys are cleared, numbers getpxexcept for unitless properties such asopacity,zIndex,flexGrowandlineHeight, and--customproperties are supported.dangerouslySetInnerHTML={{ __html }}setsinnerHTML.onXprops attach listeners.onDoubleClickmaps todblclick, and anonXCapturesuffix listens in the capture phase. Each element keeps one listener per event and only swaps the handler it calls, so inline arrow functions cost nothing extra on re-render.refaccepts auseRefbox or a callback. A function component receivesrefas an ordinary prop, so it can forward it to a child element.keymatches list items across renders. A duplicated key is treated as a new item.
Router
<Router routes={[{ path: '/player/:id', component: ({ id }) => <Player id={id} /> }]} notFound={() => <NotFound />} />navigate(to, { replace?, scroll? })pushes or replaces a history entry.scroll: truescrolls to the top.<Link to replace scroll>does the same on click. Modified clicks and links with atargetother than_selfare left to the browser.useLocation()returns the current pathname and re-renders on every navigation, including query-only changes.
