{"_id":"@audinue/redraw","name":"@audinue/redraw","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@audinue/redraw","version":"0.1.0","description":"Mini React/Mithril clone: redraw on event, state = plain object (useRef + useContext), tanpa dependency runtime.","type":"module","main":"./dist/redraw.min.js","module":"./dist/redraw.min.js","types":"./dist/redraw.d.ts","exports":{".":{"types":"./dist/redraw.d.ts","import":"./dist/redraw.min.js"},"./jsx-runtime":{"types":"./dist/jsx-runtime.d.ts","import":"./dist/jsx-runtime.min.js"},"./jsx-dev-runtime":{"types":"./dist/jsx-runtime.d.ts","import":"./dist/jsx-runtime.min.js"}},"scripts":{"build":"bun run build:js && bun run build:dts","build:js":"bun build src/index.ts --minify --outfile dist/redraw.min.js && bun build src/jsx-runtime.ts --minify --outfile dist/jsx-runtime.min.js","build:dts":"dts-bundle-generator --project tsconfig.json -o dist/redraw.d.ts src/index.ts && dts-bundle-generator --project tsconfig.json -o dist/jsx-runtime.d.ts src/jsx-runtime.ts","prepublishOnly":"bun run build","test":"bun test --parallel"},"keywords":["redraw","mithril","react","ui","jsx","bun","framework"],"license":"MIT","publishConfig":{"access":"public"},"devDependencies":{"@types/bun":"^1.4.0","dts-bundle-generator":"^9.5.1","happy-dom":"^20.12.0","typescript":"^5.5"},"_id":"@audinue/redraw@0.1.0","gitHead":"634db909dee425682bb859cf13ec5216863785b1","_nodeVersion":"22.23.2","_npmVersion":"10.9.8","dist":{"integrity":"sha512-YpQ4HPVMqMkucNF2Ff66ZW3cWlKaov1GS2z3YkqY8j1qk4DR+NmRQfx9ynLno0lJfb1Y4GNVQhS32NQnaVhKTQ==","shasum":"56b1d8c5cc88b6cc617c15b46128fea618cfd500","tarball":"https://registry.npmjs.org/@audinue/redraw/-/redraw-0.1.0.tgz","fileCount":7,"unpackedSize":29889,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFM+88mTirlLUt2n6LXJx1KHQmmVEyEsx7FVCSCrETvUAiEA0vfNyPF/amdx/0VOhZZAZEVXLvBXkt/8/wI7OvatE5I="}]},"_npmUser":{"name":"audinue","email":"audinue@gmail.com"},"directories":{},"maintainers":[{"name":"audinue","email":"audinue@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/redraw_0.1.0_1788093542517_0.16769831813038594"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-30T12:39:02.306Z","0.1.0":"2026-08-30T12:39:02.648Z","modified":"2026-08-30T12:39:02.875Z"},"maintainers":[{"name":"audinue","email":"audinue@gmail.com"}],"description":"Mini React/Mithril clone: redraw on event, state = plain object (useRef + useContext), tanpa dependency runtime.","keywords":["redraw","mithril","react","ui","jsx","bun","framework"],"license":"MIT","readme":"# redraw 🔴\n\nLibrary UI JavaScript/TypeScript ala **Mithril** yang dibangun dari nol di atas **Bun**.\nKecil (≈3 KB brotli), tanpa dependency runtime, pake **JSX**. Inti filosofinya:\n\n> **UI = f(view model)** → setiap **event** otomatis me-*redraw* semua root.\n> Lo gak perlu nyetel state internal per-komponen; cukup mutasi state biasa\n> (objek, `useRef`, atau context), dan event yang ngerender ulang.\n\nBeda paling penting sama React: **gak ada `useState` / `useReducer` / `useSyncExternalStore`**.\nState tinggal di `useRef` (mutable) + `useContext` (tree-scoped pass-through), dan **event = redraw**.\n\nKarena state **gak nge-trigger render**, view model bisa **plain object** — tanpa\nimmutable wrapper ala zustand, tanpa proxy/reactive wrapper ala vue-reactivity atau\nknockoutjs. Cukup class/POJO biasa, langsung di-mutasi. Yang render itu event, bukan wrapper.\n\n---\n\n## Instalasi & Setup\n\n```bash\nbun add @audinue/redraw\n```\n\nConfig `tsconfig.json` biar JSX-nya kepake otomatis:\n\n```jsonc\n{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@audinue/redraw\",\n    \"moduleResolution\": \"bundler\"\n    // kalo pake monorepo, arahin paths ke src:\n    // \"paths\": { \"@audinue/redraw\": [\"./src/index.ts\"] }\n  }\n}\n```\n\n---\n\n## Quickstart\n\n```tsx\nimport { createRoot, useRef } from '@audinue/redraw';\n\nfunction Counter() {\n  const n = useRef(0);          // state = useRef, mutable\n  return (\n    <div>\n      <p>Hitungan: {n.current}</p>\n      <button onClick={() => { n.current++; }}>\n        +1\n      </button>\n    </div>\n  );\n}\n\ncreateRoot(document.getElementById('app')!).render(<Counter />);\n```\n\nCoba klik tombolnya → UI langsung ke-update. **Lo gak manggil render manual**\n— event wrapper-nya yang manggil `redraw()` buat lo.\n\n---\n\n## Konsep inti\n\n### 1. Redraw on event (default)\n\nSemua handler event (`onClick`, `onInput`, dll) otomatis memicu render ulang\n**semua root yang ke-mount** setelah handler selesai. Jadi lo cukup mutasi state\ndi handler, gak perlu mikirin kapan harus render.\n\nKalo mau **skip** redraw otomatis di event tertentu:\n\n```tsx\n<button onClick={(e: any) => {\n  e.redraw = false;     // ← event ini gak ngerender\n  // mutasi state tanpa re-render…\n}}>…</button>\n```\n\n### 2. Redraw manual — `redraw()`\n\nBuat update yang **bukan** dari event DOM (timer, `setInterval`, `setTimeout`,\nrespon WebSocket, dll), panggil `redraw()` langsung:\n\n```tsx\nimport { redraw, useRef, useEffect } from '@audinue/redraw';\n\nfunction Clock() {\n  const now = useRef(new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      now.current = new Date();\n      redraw();                    // ← manual: render ulang semua root\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return <p>{now.current.toLocaleTimeString()}</p>;\n}\n```\n\n### 3. State: `useRef` + `useContext` (gak ada useState)\n\nKarena redraw terjadi via event, state **gak perlu nge-trigger render sendiri**.\nDua cara nyimpen state:\n\n- **`useRef`** — state lokal/mutable per-komponen. Dibaca tiap render (`ref.current`),\n  dimutasi di event handler.\n- **`useContext`** — state tree-scoped (diteruskan ke subtree). Provider cuma\n  **nge-pass value**, dia gak nge-redraw; render tetep dipicu event/`redraw()`.\n\nKonsekuensinya: view model bisa **plain object** — class/POJO biasa yang\ndi-mutasi langsung (`vm.count++`, `vm.items.push(...)`). Gak perlu:\n\n- **immutable wrapper** kayak zustand (`setState`/produce) — karena gak ada\n  \"deteksi perubahan\" yang harus nyetel render;\n- **proxy/reactive wrapper** kayak vue-reactivity / knockoutjs — karena yang\n  render itu event, bukan observer yang mastiin data \"berubah\".\n\nSatu-satunya trade-off: update yang datang **di luar event DOM** (async kayak\n`fetch` callback, event source, WebSocket) butuh `redraw()` manual:\n\n```tsx\nsocket.onmessage = () => {\n  vm.messages.push(socket.data);\n  redraw();   // ← manual, karena ini bukan event DOM\n};\n```\n\nTapi acceptable — **sebagian besar operasi UI itu sinkron** (klik, ketik, submit),\njadi hampir selalu otomatis.\n\nContoh kombinasi (theme switcher):\n\n```tsx\nimport { createContext, useContext, useRef } from '@audinue/redraw';\n\nconst Theme = createContext('light');\n\nfunction Panel() {\n  const theme = useContext(Theme);\n  return <div className={theme === 'dark' ? 'dark' : 'light'}>…</div>;\n}\n\nfunction App() {\n  const theme = useRef('light');\n  return (\n    <Theme.Provider value={theme.current}>\n      <Panel />\n      <button onClick={() => { theme.current = 'dark'; }}>gelap</button>\n    </Theme.Provider>\n  );\n}\n```\n\n---\n\n## API Reference\n\n### Entry points\n- `redraw` (default) — semua API di bawah.\n- `@audinue/redraw/jsx-runtime` & `@audinue/redraw/jsx-dev-runtime` — dipakai otomatis oleh JSX compiler.\n\n### Mount / render\n\n| API | Deskripsi |\n|---|---|\n| `createRoot(container)` | Bikin root; return `{ render(vnode), unmount() }`. Bisa render ulang berulang. |\n| `render(vnode, container)` | Shorthand: `createRoot(container).render(vnode)`. |\n| `redraw()` | MarkDirty semua root aktif + render (batch per microtask). |\n| `flushSync(fn)` | Jalanin `fn` terus render **sinkron** (bypass batching). |\n\n```tsx\nconst root = createRoot(el);\nroot.render(<App />);\n// … nanti:\nroot.render(<App lagi />);\n// … mau bongkar:\nroot.unmount();\n```\n\n### Hooks\n\n| Hook | Fungsi |\n|---|---|\n| `useRef(initial)` | Object `{ current }` mutable, stabil antar render. **Ini \"state\"-nya.** |\n| `useContext(ctx)` | Baca value context terdekat (fallback ke default). |\n| `useEffect(fn, deps?)` | Side-effect setelah render; cleanup via return. |\n| `useLayoutEffect(fn, deps?)` | Sama, tapi jalan sinkron sebelum paint. |\n| `useCallback(fn, deps)` | Cache fungsi biar stabil antar render. |\n| `useMemo(fn, deps)` | Cache hasil kalkulasi. |\n| `useId()` | ID unik per-komponen. |\n\n### Context\n\n```tsx\nconst Ctx = createContext(defaultValue);\n// Provider:\n<Ctx.Provider value={someValue}>…</Ctx.Provider>\n// Konsumen:\nconst v = useContext(Ctx);\n```\n\n### Komponen lanjutan\n\n| API | Fungsi |\n|---|---|\n| `Fragment` (`<>…</>`) | Render banyak node tanpa wrapper. |\n| `createPortal(children, container)` | Render ke node DOM lain (modal, tooltip). |\n| `forwardRef(render)` | Teruskan `ref` ke child. |\n| `memo(Component, compare?)` | Skip render kalo props shallow-equal. |\n\n### Events\n\nPakai props berawalan `on`, event DOM normal (`onClick`, `onInput`, `onChange`,\n`onSubmit`, dll). Handler lama otomatis dilepas pas props update.\n\n```tsx\n<input value={ref.current}\n       onInput={(e: any) => { ref.current = e.target.value; }} />\n```\n\n> **Catatan `onChange`:** untuk `<input type=\"checkbox|radio\">` dan `<select>`,\n> `onChange` dipetakan ke event `change` native. Untuk input teks, pakai `onInput`.\n\n---\n\n## Contoh app\n\nLengkap ada di folder [`example/`](example/): stopwatch+counter, todo (keyed diff),\ntheme+portal, dan **online shop** (view model class-based + user flow test).\nJalanin:\n\n```bash\ncd example && bun install && bun run dev\n# buka http://localhost:8899\n```\n\n---\n\n## Testing\n\nPake `bun:test` + happy-dom. Lihat `test/` (unit library) dan `example/test/`\n(user flow view model):\n\n```bash\nbun test          # dari root (library)\ncd example && bun test   # user flow contoh app\n```\n\n---\n\n## View model: plain object, bukan state wrapper\n\nDi library lain, state butuh \"bungkus\" biar render bisa dipicu:\n\n- **React** (zustand, redux, dll): butuh **immutable update** — kamu gak boleh\n  mutate langsung, harus bikin object baru (`setState`, `produce`, selector baru)\n  biar React tahu ada perubahan lewat referensi.\n- **Vue** (vue-reactivity) & **Knockout** (observables): butuh **proxy/observable\n  wrapper** — akses property di-track biar library tahu siapa yang depend ke mana.\n\nDi `redraw` dua-duanya **gak perlu**, karena render-nya bukan reaksi terhadap\n\"data berubah\", tapi reaksi terhadap **event**. Setiap event → seluruh root\ndi-render ulang dari view model saat itu, apa pun yang berubah (atau bahkan gak\nberubah). Jadi view model cukup jadi **plain object**: class/POJO biasa, tanpa\nwrapper, di-mutasi langsung:\n\n```ts\n// Gak perlu immutable (gak perlu setState/produce) —\n// langsung mutate aja, event yang ngerender.\ncart.items.push({ product, qty: 1 });\ncart.total; // dibaca tiap render\n```\n\nKonsekuensinya enak banget:\n\n- **Testable**: user flow bisa di-test langsung ke object tanpa browser/DOM.\n- **Simpel**: gak ada jebakan referensi, gak ada tracking dependensi.\n- **Portable**: view model bisa di-share lintas framework atau di pakai di Node.\n\nContoh lengkap: `example/src/shop-vm.ts` (online shop, class-based) + `example/test/shop.test.ts`.\n\n---\n\n## Kenapa gak ada useState?\n\nKarena redraw udah otomatis via event, hook state yang nge-trigger render\nsendiri (`useState`/`useReducer`) cuma redundant. State cukup `useRef`/`useContext`\n+ view model plain object — gak perlu hook state yang mastiin \"render kalo data ini berubah\".\n\nKalau modelnya udah \"event = redraw\", nambahin reactivity/immutability cuma\nnambah kompleksitas tanpa nilai: lo mutasi plain object, event render ulang,\nselesai. Zustand/vue-reactivity/knockout nyelesaiin masalah yang di sini **memang\ngak ada** (nge-trigger render dari perubahan data).\n\n---\n\n## Lisensi\n\nMIT\n","readmeFilename":"README.md","_rev":"1-e748f8e290799c3617af40345425825c"}