npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@audinue/redraw

v0.1.0

Published

Mini React/Mithril clone: redraw on event, state = plain object (useRef + useContext), tanpa dependency runtime.

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/redraw

Config 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>, onChange dipetakan ke event change native. Untuk input teks, pakai onInput.


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:8899

Testing

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 app

View 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 render

Konsekuensinya 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