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

@devmore/vanilact

v1.0.18

Published

Pure vanila javascript.

Readme

Vanilact.js

A lightweight, React-inspired frontend framework built on vanilla TypeScript. Vanilact.js brings familiar component patterns — hooks, routing, lazy loading, and class-based components — with zero dependencies and a minimal footprint.


Table of Contents


Features

  • Zero dependencies — pure vanilla TypeScript
  • ⚛️ React-like APIuseState, useEffect, createElement, and more
  • 🧭 Built-in client-side router — with dynamic route parameters and middleware support
  • 🏛️ Class & function components — choose your style
  • 🔗 Refs — direct DOM element access via createRef
  • 💤 Lazy loading — dynamic imports with lazy()
  • 🧩 Fragment support — render multiple children without a wrapper node

Getting Started

import { createApp, createElement } from './vanilact';

function App() {
  return createElement('h1', null, 'Hello, Vanilact!');
}

const root = document.getElementById('app') as HTMLElement;
createApp(root).render(App);

Core Concepts

Creating an App

Use createApp to mount your application to a DOM element, then call .render() with your root component.

import { createApp } from './vanilact';
import App from './App';

createApp(document.getElementById('app')).render(App);

Function Components

Function components are plain functions that return a virtual DOM node created with createElement.

import { createElement, useState } from './vanilact';

function Counter() {
  const [count, setCount] = useState(0);

  return createElement(
    'div', null,
    createElement('p', null, `Count: ${count}`),
    createElement('button', { onClick: () => setCount(count + 1) }, 'Increment')
  );
}

Class Components

Extend IComponent for class-based components. Override willMount, render, and onMount for lifecycle control.

import { IComponent, createElement } from './vanilact';

class MyComponent extends IComponent {
  willMount() {
    console.log('Before render');
  }

  render() {
    return createElement('p', null, 'Hello from a class component!');
  }

  onMount() {
    console.log('After render — DOM is ready');
  }
}

| Method | When it runs | |---|---| | willMount() | Before the component renders | | render() | Returns the virtual DOM tree | | onMount() | After the component is added to the DOM | | onUpdate() | After an existing component instance updates | | onUnmount() | Releases resources when the component is removed | | getDom(selector?) | Query a child element (uses querySelector) | | getDomAll(selector) | Query all matching children (uses querySelectorAll) |


JSX / createElement

Vanilact uses createElement as its virtual DOM factory — the same role React.createElement plays in React.

createElement(type, props, ...children)
// Renders: <a href="/home" class="nav-link">Home</a>
createElement('a', { href: '/home', class: 'nav-link' }, 'Home');

If you configure a JSX transform to map to createElement, you can use JSX syntax directly in .tsx files.


Fragment

Render multiple children without adding a wrapper element to the DOM.

import { Fragment, createElement } from './vanilact';

function List() {
  return createElement(Fragment, null,
    createElement('li', null, 'Item 1'),
    createElement('li', null, 'Item 2'),
  );
}

Hooks

Hooks must be called at the top level of a function component, in the same order on every render.

useState

const [value, setValue] = useState(initialValue);

Returns the current state value and a setter. Calling the setter triggers a re-render.

const [name, setName] = useState('World');
// Later:
setName('Vanilact');

useEffect

useEffect(callback, deps);

Runs callback after render. If deps change between renders, the effect re-runs. Return a cleanup function to run before the next effect or unmount.

useEffect(() => {
  const id = setInterval(() => console.log('tick'), 1000);
  return () => clearInterval(id); // cleanup
}, []);

useLocation

const pathname = useLocation();

Returns the current window.location.pathname and re-renders whenever it changes via popstate events.


Routing

Router

Define your routes as an array of { path, component, middlewares? } objects and pass them to Router.

import { Router, createElement } from './vanilact';
import Home from './pages/Home';
import Profile from './pages/Profile';
import Dashboard from './pages/Dashboard';

function isLoggedIn() {
  return !!localStorage.getItem('token');
}

function App() {
  return createElement(Router, {
    routes: [
      { path: '/', component: Home },
      { path: '/profile/:id', component: Profile },
      { path: '/dashboard', component: Dashboard, middlewares: [isLoggedIn] },
    ],
    errorViews: [
      { statusCode: 401, component: Unauthorized },
      { statusCode: 404, component: NotFound },
    ],
  });
}

Dynamic route params are available on the component's params prop:

function Profile({ params }) {
  return createElement('p', null, `User ID: ${params.id}`);
}

Middlewares are guard functions that return a truthy value to allow access, or falsy to block with a 401 view.


navigate

Programmatically navigate to a route and trigger a re-render.

import { navigate } from './vanilact';

navigate('/profile/42');

// With query parameters:
navigate('/search', { q: 'vanilact', page: '1' });
// → /search?q=vanilact&page=1

Refs

createRef

Get a direct reference to a DOM element.

import { createRef, createElement, IComponent } from './vanilact';

class MyForm extends IComponent {
  inputRef = createRef();

  render() {
    return createElement('input', { ref: this.inputRef, type: 'text' });
  }

  onMount() {
    this.inputRef.current?.focus();
  }
}

createRef also accepts a callback ref in props:

createElement('input', {
  ref: (el) => console.log('Got element:', el)
});

Lazy Loading

Dynamically import a component and render a placeholder until it loads.

import { lazy, createElement } from './vanilact';

const HeavyPage = lazy(() => import('./pages/HeavyPage'));

function App() {
  return createElement(HeavyPage, {});
}

Lifecycle & Setup

onSetup

Queue a function to run once after the current render cycle completes. Register it again during rendering if it should run after every update. Useful for third-party library initialisation that needs the DOM to be ready.

import { onSetup } from './vanilact';

onSetup(() => {
  // e.g. initialise a charting library
  Chart.init('#my-chart');
});

useRender

Manually render a component into an arbitrary container and run its effects. Useful for portals or injecting components outside the main tree.

import { useRender, createElement } from './vanilact';

const modal = document.getElementById('modal-root');
const disposeModal = useRender(createElement(MyModal, { title: 'Hello' }), modal);

// Updates the existing component and preserves its state.
useRender(createElement(MyModal, { title: 'Updated' }), modal);

// When the modal is no longer needed:
disposeModal();

Mixing imperative DOM code and components (version.1)

Each createApp or useRender container owns a persistent render tree. State setters update their owning root. Updates requested during rendering, lifecycle callbacks, setup callbacks, or effects are queued until the current commit finishes. Outside a commit, updates remain synchronous. The runtime patches existing DOM nodes and retains class instances, so unrelated updates preserve native input state, focus, imperative listeners, and widget content.

Use a dedicated container for injected components or third-party widgets. Avoid having both declarative children and an injected tree control the same content. useRender preserves preexisting vanilla DOM siblings; repeated calls update its owned tree rather than append additional trees. Use separate containers for independent injections. createApp replaces existing container contents on its first render.

An effect can own an external injection and return its disposer:

function App() {
  useEffect(() => {
    return useRender(createElement(MyModal, { title: 'Hello' }), modal);
  }, []);
  return createElement('main', null, 'Application');
}

const app = createApp(document.getElementById('app'));
app.render(App);
// Later: app.unmount();

Injected roots inside a host element removed by the framework are disposed automatically. For external containers, or containers removed through vanilla DOM APIs, call the returned disposer yourself. Cleanup runs once, and setters retained by asynchronous code do nothing after their component is unmounted.

Class willMount and onMount now run once per instance. Move work that must run on later updates into onUpdate, and release imperative resources in onUnmount. Props are available as this.props. getDom() retains its existing parent-container semantics; use refs when you need a specific element.

Hooks must be called in a consistent order while a component renders. useEffect without dependencies runs after every commit; [] runs on mount, with cleanup on unmount; a dependency array runs cleanup and the new effect when its values change. An effect that changes a dependency on every execution still creates an application feedback loop. The renderer stops repeated updates with a diagnostic error. Use stable component definitions and sibling key props when reordering lists.

Uncaught render or effect errors dispose the affected root and propagate to the caller. After handling the error, use createApp again to mount a fresh root. There is no component error-boundary API in this version.

Compatibility: raw HTML strings are still supported and must contain trusted markup only. This is not a sanitizer. Existing code relying on remounting after every state update, or repeated useRender calls appending copies, needs migration. These runtime changes apply to the default/version.1 API; the separate version.2 implementation is unchanged. Validate the updated runtime in your application’s staging environment before upgrading production.


API Reference

| Export | Type | Description | |---|---|---| | createApp(root) | Function | Mounts the app to a DOM element | | createElement(type, props, ...children) | Function | Creates a virtual DOM node | | useState(initialValue) | Hook | Local component state | | useEffect(callback, deps) | Hook | Side effects after render | | useLocation() | Hook | Current pathname, reactive to navigation | | navigate(path, params?) | Function | Programmatic navigation | | Router({ routes, errorViews? }) | Component | Client-side route renderer | | createRef(initial?) | Function | Creates a { current } ref object | | lazy(importFn) | Function | Wraps a dynamic import for deferred loading | | Fragment({ children }) | Component | Renders children without a wrapper element | | onSetup(fn) | Function | Queues a post-render setup callback | | useRender(component, container) | Function | Manually renders into a DOM container | | IComponent | Class | Base class for class-based components | | isHTML(str) | Utility | Returns true if a string contains HTML | | isClassComponent(component) | Utility | Returns true if a value is a class component |


License

MIT