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

@granularjs/core

v1.6.7

Published

JS-first frontend framework with granular reactivity. No Virtual DOM, no build step, no magic - just explicit reactivity and direct DOM updates.

Readme

Granular Framework (WIP)

Granular is a JS-first frontend framework built for performance, clarity, and real control. No template DSL, no VDOM, no magic compile step - just explicit reactivity and direct DOM updates.

For those of you tired of being "markup organizers", tired of fighthing against re-render mess, tired of 1GB of node_modules to make a 500kb application, layers and layers of compilation, no control over the end result of your code, Granular brings coding to the engineering level again. Code looks like code on Granular, and when you look at the code you just know what will happen. No need to figure out a one hundred steps "lifecycle".

If your UI should be fast and your code should still look like code, this is for you.

For AI coding assistants: In this repo, for full API and patterns, see GRANULAR_AI_GUIDE.md and ARCHITECTURE.md.

The Pitch

  • JS-first UI: DOM tags are functions (Div, Span, Button).
  • Granular updates: only the nodes that change update.
  • Explicit reactivity: signal, state, after, before, set, compute, persist.
  • No JSX/TSX: no parallel language, no VDOM tree.
  • No build required: runs directly in the browser (ESM).
  • No dependency pile-up: no 300‑package dependency tree just to render a button.

Quick Start

Create a new Granular app with Vite:

npm create @granularjs/app my-app
cd my-app
npm run dev

Or install in an existing project:

npm install @granularjs/core @granularjs/ui

This creates a new project with:

  • Vite dev server with hot reload
  • granular + @granular/ui
  • Pre-configured routing
  • Example pages with reactivity demos

A Tiny Example

const App = () => {
  const counter = persist(state(0), { key: 'counter' });

  before(counter).change((next) => {
    return (next <= 10)
  })

  after(counter).change(() => {
    console.log('counter changed')
  });

  const doubled = after(counter).compute((value) => value * 2);

  return Div({ style: { fontSize: 20 } },
    Span(counter, ' x2 = ', doubled),
    Button({ onClick: () => counter.set(counter.get() + 1) }, 'Increment')
  );
};

Why Granular (not React)

  • No virtual DOM: no reconciler, no tree diff, no “render” ceremony.
  • No build tax: skip the compile pipeline and ship ESM directly.
  • Real performance: update only what changed, not an entire tree.
  • Explicit, readable reactivity: after(...targets) and after(...targets).compute(...).
  • Fewer moving pieces: no metaframework, no plugin circus, no “install 738 packages”.
  • Functional ergonomics: clean JS with predictable behavior (and no hook rules).

Yes, we are poking the bear - but for a reason. Complexity and over‑abstraction are not features.

What’s in the Box

  • Core runtime: DOM tags, renderables, granular updates.
  • State: state() + computed values + persistence.
  • Context: share reactive state across a component tree without prop drilling.
  • Query/Refetch: caching, dedupe, retries.
  • Router: history/hash/memory with guards and transitions.
  • Events: before/after hooks everywhere (variadic).
  • SSR: renderToString + hydrate for server HTML.
  • WebSockets: client with reconnect + hooks.

Reactive API (quick)

const total = after(cart.items, cart.discount).compute((items, discount) => {
  return calcTotal(items, discount);
});

const unsub = after(user.name, user.role).change((next) => {
  console.log('changed:', next);
});

before(form.values).change((next) => {
  if (!next.name) return false;
});

What the Framework Delivers

JS‑First DOM Rendering

  • DOM tags are functions (Div, Span, Button, ...).
  • Each tag accepts any number of arguments in any order: props objects (HTML attributes) and children (text, renderables, signals, state, arrays). Examples: Div('Test'), Div({ style: { width: '100px' } }, 'Texto', { className: 'teste' }, 'Mais Texto').
  • Props are applied directly to the real DOM.
  • Children accept primitives, renderables, arrays, and observable sources.
  • No HTML template parsing, no VDOM. Granular renders real DOM, on demand, with zero template gymnastics. Your UI is JavaScript, nothing else.

Renderable Contract

  • Renderable is the base mountable unit.
  • Renderer normalizes values:
    • primitive → TextNode
    • Node → mount directly
    • Renderable → mount/unmount lifecycle
    • Array → flattened list This keeps rendering predictable and composable, without hidden layers.

SSR (Server‑Side Rendering)

renderToString(renderable):

  • Generates HTML without a DOM.
  • Works with all built‑in renderables.

hydrate(target, renderable):

  • Attaches UI on the client after SSR.

Example:

const html = renderToString(App({ data }));

DOM Utilities

  • Elements exposes all tag functions in a single object.
  • Renderer.normalize() accepts primitives, nodes, renderables, and arrays.

DOM Node Access

Use node to capture the underlying DOM element into a reactive target. It accepts a state or signal and is set when the element mounts.

Example:

import { Div, state } from '@granularjs/core';

const rootEl = state(null);

Div({ node: rootEl }, 'Hello');

Function Components

Plain functions:

  • Components are just functions that return renderables or DOM nodes.
  • One‑time construction of the view.
  • Updates are granular; no re‑render loop.
  • Uses state() and after/before/set. Your component runs once. The DOM updates forever. That is the whole point.

Signals and State

signal(value) and state(value):

  • signal is a small observable primitive.
  • state provides proxy paths with .get() / .set() and read‑only bindings.
  • .get() and .set() are path-relative: calling them from a nested path resolves from that path, not the root.
  • Direct mutation of state paths is forbidden (s.user = ... throws).
  • mutate(optimistic, mutation, options?) supports optimistic updates with rollback.
  • subscribe(target, selector?, listener?, equalityFn?) subscribes to a reactive target with optional selector for fine-grained updates. You get mutable ergonomics with immutable safety. No spread hell, no guesswork.

Resolve and Computed

  • resolve(value) unwraps any reactive value (signal, state, computed, state path) to its raw current value. Non-reactive values pass through unchanged.
  • computed(input) transforms a props object into a proxy where each property becomes a read-only computed state. Accepts signals, state, or plain objects.

Concat

concat(...parts, options):

  • Joins primitives and reactive values into a single reactive string.
  • Supports conditional tuples: [state, 'class-name'].
  • Options: separator, filterFalsy.

Type Guards

  • isSignal(value) - true if value is a signal.
  • isState(value) - true if value is a state root.
  • isStatePath(value) - true if value is a state path (e.g., user.name).
  • isComputed(value) - true if value is a computed state.

Low-level Signal API

readSignal(sig) and setSignal(sig, next, force?):

  • Direct read/write access to a signal's value.
  • setSignal with force = true fires subscribers even when the value is unchanged.
  • Exported for library/advanced use (e.g., custom renderables, context adapters). Prefer state() for application code.

Reactive Observers

after(...targets) / before(...targets):

  • Variadic targets (any change triggers).
  • change(fn) receives (next, prev, ctx).
  • compute(fn, options) returns a read‑only, state‑like computed value.
  • before can cancel by returning false.
  • For arrays, next and prev are lazy (next() / prev()).

change() - precise change handling

  • next and prev are values for signals/state.
  • For arrays, next/prev are functions to avoid heavy snapshots.
  • ctx includes metadata (for arrays: ctx.patch, prevLength, nextLength).

compute() - derived state with intent

  • Same next/prev/ctx contract as change().
  • Supports async, debounce, hash, equality checks, and error handling.

Array patch quick reference

  • insert: { type, index, items }
  • remove: { type, index, count, items }
  • set: { type, index, value, prev }
  • reset: { type, items, prevItems }

before() - control flow that no other framework has

  • Runs before the change is committed.
  • Returning false cancels the change completely.
  • This is not a hook. It is a guardrail.
  • It lets you enforce business rules, confirm actions, block invalid state, and keep UI clean without hacks.
  • Think of it as an interceptor for state: the mutation only happens if you allow it.

after() - deterministic reactions

  • Runs after the change is applied.
  • Great for side effects, analytics, syncing, or derived updates.
  • No re-render, no virtual tree - just a direct reaction to the exact change.

Computed / Derived State

after(...targets).compute(fn, options) and before(...targets).compute(fn, options):

  • Recomputes when any target changes.
  • fn(next, prev, ctx) for a single target.
  • fn(nextList, prevList, ctxList) for multiple targets.
  • Supports async functions (last‑write‑wins).
  • Returns a read‑only, state‑like value with .get() and bindings. This is how you build reactive values without re-rendering anything.

Options:

  • debounce delay
  • hash(...args) skip if unchanged
  • equals(prev, next) skip if unchanged
  • onError(err) for sync/async errors

Collections and Lists

observableArray(initial):

  • Emits patches (insert, remove, set, reset).
  • Supports before() / after() hooks.

list(items, renderItem):

  • Efficient list rendering from observable arrays, signals, or state.
  • Each item is wrapped in state(item) and each index in signal(index).
  • renderItem receives (itemState, indexSignal) - reactive wrappers, not raw values.
  • On set patches, the existing state is updated (itemState.set(newValue)), so only the specific DOM nodes bound to changed properties update. No DOM destruction/recreation.
  • Use state paths for reactive bindings: Span(item.name) updates only that text node.
  • .get() is path-relative: item.name.get() returns the name value, item.get() returns the item object.
  • Use .get() inside event closures for raw values: onClick: () => doSomething(item.id.get()).

Example:

const todos = observableArray([{ text: 'Learn', done: false }]);

list(todos, (todo) => Div(
  Span(todo.text),                                  // reactive binding
  Span(after(todo.done).compute(d => d ? '✓' : '○')), // reactive computed
  Button({ onClick: () => todo.set().done = !todo.done.get() }, 'Toggle')
))

todos.push({ text: 'Build', done: false }); // only adds new DOM
todos[0] = { text: 'Master', done: true };  // only updates bound text nodes

when(condition, renderTrue, renderFalse):

  • Reactive conditional rendering without re‑rendering parents. Granular treats lists as live data structures, not as arrays you re‑map on every tick.

Virtualization / Windowing

virtualList(items, options):

  • Optional fixed itemSize (measured automatically if omitted).
  • Supports direction: 'vertical' | 'horizontal'.
  • Viewport size is derived from the parent element.
  • Only visible items are rendered (overscan supported).

Example:

virtualList(rows, {
  render: (row) => Row(row),
  itemSize: 48,
  direction: 'vertical',
  overscan: 2,
});

Horizontal example (auto size):

virtualList(cards, {
  render: (card) => Card(card),
  direction: 'horizontal',
  overscan: 3,
});

Context

context(defaultValue):

  • Shares reactive state across a component tree without prop drilling.
  • scope(value?) creates a provider level with .get(), .set(), path access, and .serve(renderable).
  • state() returns a reactive state bound to the nearest ancestor provider.
  • Supports nesting: inner scopes override outer ones without affecting siblings.
  • Works with dynamic children (list(), when()) via mount-time resolution. Context gives you React-like sharing without React-like complexity. No Provider JSX, no useContext - just state that flows.

Example:

import { context, Div, Text, after } from '@granularjs/core'

const themeCtx = context('light')

const ThemeProvider = (...children) => {
  const theme = themeCtx.scope('dark')
  return theme.serve(Div(...children))
}

const ThemedCard = () => {
  const theme = themeCtx.state()
  return Div(
    { className: after(theme).compute(t => `card card-${t}`) },
    Text('Current theme: ', theme)
  )
}

// Usage
ThemeProvider(ThemedCard())

Provider controls its own state:

const sizeCtx = context([])

const Table = (...children) => {
  const sizes = sizeCtx.scope(['1fr', '2fr', 'auto'])
  // sizes.get(), sizes.set(), sizes[0] - full state API
  return sizes.serve(Div(...children))
}

const Row = () => {
  const sizes = sizeCtx.state()
  // sizes is reactive, bound to the nearest Table's scope
  return Div({ style: { gridTemplateColumns: after(sizes).compute(s => s.join(' ')) } })
}

State as Store

Granular does not need a separate store type. Any state() can be your global store.

Example (singleton module store):

// user.store.js
export const userStore = state({ users: [] });

export const addUser = (user) => userStore.set().users = userStore.get().users.concat(user);
export const removeUser = (id) =>
  userStore.set().users = userStore.get().users.filter((u) => u.id !== id);

Selectors:

const users = subscribe(userStore, (s) => s.users);

Query / Refetch

QueryClient:

  • Cache per key
  • Dedupe in‑flight requests
  • Retry with backoff
  • staleTime, cacheTime
  • invalidate and refetch
  • Refetch on focus/reconnect
  • Abortable fetch via AbortController
  • Service factory with endpoint maps and middlewares Server state is not special. It is just state with guarantees.

Service example:

const userService = queryClient.service({
  baseUrl: '/api',
  middlewares: [authMiddleware],
  endpoints: {
    getUsers: { path: '/users', method: 'GET', map: UserDTO.from },
    getUser: { path: '/users/:id', method: 'GET', map: UserDTO.from },
    createUser: { path: '/users', method: 'POST', map: UserDTO.from },
  },
});

const user = await userService.getUser({
  params: { id: 1 },
  query: { active: true },
  headers: { 'X-Trace': '1' },
});

Router

Router / createRouter:

  • History, hash, and memory modes
  • Guards, redirects, loaders
  • Transition hooks
  • Scroll restoration
  • Safe path matching with priorities
  • Nested routes with children
  • Layouts via layout(outlet, ctx)
  • Query syncing via router.queryParameters() Navigation stays declarative, but the runtime stays in your control.

Example:

const AppLayout = (outlet) => Div(
  Sidebar(),
  Div({ className: 'content' }, outlet)
);

const SettingsLayout = (outlet) => Div(
  H2('Settings'),
  outlet
);

const router = createRouter({
  mode: 'history',
  routes: [
    {
      path: '/',
      layout: AppLayout,
      children: [
        { path: '', page: Home },
        { path: 'dashboard', page: Dashboard },
        {
          path: 'settings',
          layout: SettingsLayout,
          children: [
            { path: '', page: SettingsHome },
            { path: 'profile', page: Profile },
            { path: 'billing', page: Billing },
          ],
        },
      ],
    },
  ],
});

Query parameters:

const q = router.queryParameters({ replace: false, preserveHash: true });

Input({
  value: q.term,
  onInput: (ev) => q.set().term = ev.target.value,
});

Button({ onClick: () => q.set().page = 1 }, 'Reset page');

Events

EventHub:

  • Fluent before() / after() hooks
  • Dynamic event names via Proxy One event system, used everywhere. Predictable and powerful.

Persistence / Hydration

persist(state, options):

  • Returns the same target for chaining.
  • Hydrates first, then subscribes and saves.
  • Default serializer drops functions/symbols.
  • reconcile(snapshot) can rebuild non‑serializable fields. Your app survives refreshes without manual glue code.

Example:

const profile = persist(state({ name: 'Ana', format: (v) => v.toUpperCase() }), {
  key: 'profile',
  reconcile: (snap) => ({ ...snap, format: (v) => v.toUpperCase() }),
});

Form Management

form(initial) returns:

  • values, meta, errors, touched, dirty (state‑like)
  • validators (Set with add/delete/clear)
  • reset() restores initial snapshot

Validators contract:

  • fn(values) returns true | false | string | object | Promise<...>
  • true/undefined → ok
  • false → form error (_form = true)
  • string → form error message
  • object → field errors merged by key Forms stop being a framework within the framework. This is just state, done right.

Input Formatting

Inputs accept a format prop that can be a string pattern, a regex, a formatter function, or a config object. Formatting returns { value, visual, raw } and supports mode:

  • both (default): state stores formatted value, input shows formatted visual
  • value-only: state stores formatted value, input shows raw
  • visual-only: state stores raw, input shows formatted visual

Pattern tokens:

  • d digit
  • a letter
  • * alphanumeric
  • s non-alphanumeric

Example:

import { Input, state } from '@granularjs/core';

const phone = state('');

Input({
  value: phone,
  format: { pattern: '(ddd) ddd-dddd', mode: 'visual-only' },
});

Optimistic Updates

state.mutate(optimistic, mutation, options?):

  • Applies the optimistic change immediately.
  • Rolls back automatically on error.
  • Optional rollback and clone for control.

Example:

await userState.mutate(
  () => userState.set().name = 'Guilherme',
  () => userService.saveUser(userState.get())
);

Error Boundaries

ErrorBoundary({ fallback, onError }, child):

  • Catches runtime errors inside a subtree.
  • Renders the fallback when an error happens.
  • onError receives the error and context.

Example:

ErrorBoundary(
  { fallback: () => Div('Ops'), onError: (err) => console.error(err) },
  () => Div('OK')
);

Portals / Overlays

portal(target, content):

  • Renders UI outside the normal DOM hierarchy.
  • target can be a selector or a DOM element. Portals are how you build modals, toasts and overlays without fighting layout or z‑index wars. Portals are renderables: they must exist in the render tree to mount, and they unmount when removed from the tree.

Example:

portal('#overlay', () => Div({ className: 'modal' }, 'Hello'));

Controlled usage (recommended):

const open = state(false);

const App = () => Div(
  Button({ onClick: () => open.set(true) }, 'Open'),
  when(open, () =>
    portal(() => Div(
      { className: 'modal' },
      Button({ onClick: () => open.set(false) }, 'Close')
    ))
  )
);

WebSockets

createWebSocket(options):

  • Auto‑connect with reconnect support.
  • before/after hooks for message and send.
  • Reactive state via ws.state().

Example:

const ws = createWebSocket({ url: 'wss://example.com' });

ws.after().message(({ data }) => {
  console.log('message', data);
});

ws.send({ type: 'ping' });