@devmore/vanilact
v1.0.18
Published
Pure vanila javascript.
Maintainers
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 API —
useState,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=1Refs
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
