@audinue/redraw
v0.1.0
Published
Mini React/Mithril clone: redraw on event, state = plain object (useRef + useContext), tanpa dependency runtime.
Maintainers
Readme
redraw 🔴
Library UI JavaScript/TypeScript ala Mithril yang dibangun dari nol di atas Bun. Kecil (≈3 KB brotli), tanpa dependency runtime, pake JSX. Inti filosofinya:
UI = f(view model) → setiap event otomatis me-redraw semua root. Lo gak perlu nyetel state internal per-komponen; cukup mutasi state biasa (objek,
useRef, atau context), dan event yang ngerender ulang.
Beda paling penting sama React: gak ada useState / useReducer / useSyncExternalStore.
State tinggal di useRef (mutable) + useContext (tree-scoped pass-through), dan event = redraw.
Karena state gak nge-trigger render, view model bisa plain object — tanpa immutable wrapper ala zustand, tanpa proxy/reactive wrapper ala vue-reactivity atau knockoutjs. Cukup class/POJO biasa, langsung di-mutasi. Yang render itu event, bukan wrapper.
Instalasi & Setup
bun add @audinue/redrawConfig tsconfig.json biar JSX-nya kepake otomatis:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@audinue/redraw",
"moduleResolution": "bundler"
// kalo pake monorepo, arahin paths ke src:
// "paths": { "@audinue/redraw": ["./src/index.ts"] }
}
}Quickstart
import { createRoot, useRef } from '@audinue/redraw';
function Counter() {
const n = useRef(0); // state = useRef, mutable
return (
<div>
<p>Hitungan: {n.current}</p>
<button onClick={() => { n.current++; }}>
+1
</button>
</div>
);
}
createRoot(document.getElementById('app')!).render(<Counter />);Coba klik tombolnya → UI langsung ke-update. Lo gak manggil render manual
— event wrapper-nya yang manggil redraw() buat lo.
Konsep inti
1. Redraw on event (default)
Semua handler event (onClick, onInput, dll) otomatis memicu render ulang
semua root yang ke-mount setelah handler selesai. Jadi lo cukup mutasi state
di handler, gak perlu mikirin kapan harus render.
Kalo mau skip redraw otomatis di event tertentu:
<button onClick={(e: any) => {
e.redraw = false; // ← event ini gak ngerender
// mutasi state tanpa re-render…
}}>…</button>2. Redraw manual — redraw()
Buat update yang bukan dari event DOM (timer, setInterval, setTimeout,
respon WebSocket, dll), panggil redraw() langsung:
import { redraw, useRef, useEffect } from '@audinue/redraw';
function Clock() {
const now = useRef(new Date());
useEffect(() => {
const id = setInterval(() => {
now.current = new Date();
redraw(); // ← manual: render ulang semua root
}, 1000);
return () => clearInterval(id);
}, []);
return <p>{now.current.toLocaleTimeString()}</p>;
}3. State: useRef + useContext (gak ada useState)
Karena redraw terjadi via event, state gak perlu nge-trigger render sendiri. Dua cara nyimpen state:
useRef— state lokal/mutable per-komponen. Dibaca tiap render (ref.current), dimutasi di event handler.useContext— state tree-scoped (diteruskan ke subtree). Provider cuma nge-pass value, dia gak nge-redraw; render tetep dipicu event/redraw().
Konsekuensinya: view model bisa plain object — class/POJO biasa yang
di-mutasi langsung (vm.count++, vm.items.push(...)). Gak perlu:
- immutable wrapper kayak zustand (
setState/produce) — karena gak ada "deteksi perubahan" yang harus nyetel render; - proxy/reactive wrapper kayak vue-reactivity / knockoutjs — karena yang render itu event, bukan observer yang mastiin data "berubah".
Satu-satunya trade-off: update yang datang di luar event DOM (async kayak
fetch callback, event source, WebSocket) butuh redraw() manual:
socket.onmessage = () => {
vm.messages.push(socket.data);
redraw(); // ← manual, karena ini bukan event DOM
};Tapi acceptable — sebagian besar operasi UI itu sinkron (klik, ketik, submit), jadi hampir selalu otomatis.
Contoh kombinasi (theme switcher):
import { createContext, useContext, useRef } from '@audinue/redraw';
const Theme = createContext('light');
function Panel() {
const theme = useContext(Theme);
return <div className={theme === 'dark' ? 'dark' : 'light'}>…</div>;
}
function App() {
const theme = useRef('light');
return (
<Theme.Provider value={theme.current}>
<Panel />
<button onClick={() => { theme.current = 'dark'; }}>gelap</button>
</Theme.Provider>
);
}API Reference
Entry points
redraw(default) — semua API di bawah.@audinue/redraw/jsx-runtime&@audinue/redraw/jsx-dev-runtime— dipakai otomatis oleh JSX compiler.
Mount / render
| API | Deskripsi |
|---|---|
| createRoot(container) | Bikin root; return { render(vnode), unmount() }. Bisa render ulang berulang. |
| render(vnode, container) | Shorthand: createRoot(container).render(vnode). |
| redraw() | MarkDirty semua root aktif + render (batch per microtask). |
| flushSync(fn) | Jalanin fn terus render sinkron (bypass batching). |
const root = createRoot(el);
root.render(<App />);
// … nanti:
root.render(<App lagi />);
// … mau bongkar:
root.unmount();Hooks
| Hook | Fungsi |
|---|---|
| useRef(initial) | Object { current } mutable, stabil antar render. Ini "state"-nya. |
| useContext(ctx) | Baca value context terdekat (fallback ke default). |
| useEffect(fn, deps?) | Side-effect setelah render; cleanup via return. |
| useLayoutEffect(fn, deps?) | Sama, tapi jalan sinkron sebelum paint. |
| useCallback(fn, deps) | Cache fungsi biar stabil antar render. |
| useMemo(fn, deps) | Cache hasil kalkulasi. |
| useId() | ID unik per-komponen. |
Context
const Ctx = createContext(defaultValue);
// Provider:
<Ctx.Provider value={someValue}>…</Ctx.Provider>
// Konsumen:
const v = useContext(Ctx);Komponen lanjutan
| API | Fungsi |
|---|---|
| Fragment (<>…</>) | Render banyak node tanpa wrapper. |
| createPortal(children, container) | Render ke node DOM lain (modal, tooltip). |
| forwardRef(render) | Teruskan ref ke child. |
| memo(Component, compare?) | Skip render kalo props shallow-equal. |
Events
Pakai props berawalan on, event DOM normal (onClick, onInput, onChange,
onSubmit, dll). Handler lama otomatis dilepas pas props update.
<input value={ref.current}
onInput={(e: any) => { ref.current = e.target.value; }} />Catatan
onChange: untuk<input type="checkbox|radio">dan<select>,onChangedipetakan ke eventchangenative. Untuk input teks, pakaionInput.
Contoh app
Lengkap ada di folder example/: stopwatch+counter, todo (keyed diff),
theme+portal, dan online shop (view model class-based + user flow test).
Jalanin:
cd example && bun install && bun run dev
# buka http://localhost:8899Testing
Pake bun:test + happy-dom. Lihat test/ (unit library) dan example/test/
(user flow view model):
bun test # dari root (library)
cd example && bun test # user flow contoh appView model: plain object, bukan state wrapper
Di library lain, state butuh "bungkus" biar render bisa dipicu:
- React (zustand, redux, dll): butuh immutable update — kamu gak boleh
mutate langsung, harus bikin object baru (
setState,produce, selector baru) biar React tahu ada perubahan lewat referensi. - Vue (vue-reactivity) & Knockout (observables): butuh proxy/observable wrapper — akses property di-track biar library tahu siapa yang depend ke mana.
Di redraw dua-duanya gak perlu, karena render-nya bukan reaksi terhadap
"data berubah", tapi reaksi terhadap event. Setiap event → seluruh root
di-render ulang dari view model saat itu, apa pun yang berubah (atau bahkan gak
berubah). Jadi view model cukup jadi plain object: class/POJO biasa, tanpa
wrapper, di-mutasi langsung:
// Gak perlu immutable (gak perlu setState/produce) —
// langsung mutate aja, event yang ngerender.
cart.items.push({ product, qty: 1 });
cart.total; // dibaca tiap renderKonsekuensinya enak banget:
- Testable: user flow bisa di-test langsung ke object tanpa browser/DOM.
- Simpel: gak ada jebakan referensi, gak ada tracking dependensi.
- Portable: view model bisa di-share lintas framework atau di pakai di Node.
Contoh lengkap: example/src/shop-vm.ts (online shop, class-based) + example/test/shop.test.ts.
Kenapa gak ada useState?
Karena redraw udah otomatis via event, hook state yang nge-trigger render
sendiri (useState/useReducer) cuma redundant. State cukup useRef/useContext
- view model plain object — gak perlu hook state yang mastiin "render kalo data ini berubah".
Kalau modelnya udah "event = redraw", nambahin reactivity/immutability cuma nambah kompleksitas tanpa nilai: lo mutasi plain object, event render ulang, selesai. Zustand/vue-reactivity/knockout nyelesaiin masalah yang di sini memang gak ada (nge-trigger render dari perubahan data).
Lisensi
MIT
