js-act
v1.0.8
Published
Lightweight SSR-first frontend library with fine-grained signals and island architecture
Downloads
160
Maintainers
Readme
js-act
Lightweight SSR-first frontend library with fine-grained signals and island architecture.
8 kB gzipped. Zero runtime dependencies. Works with a script tag or npm.
Create a new app
The fastest way to get started — scaffold a project in one command:
npx js-act create my-app
cd my-app
npm install
npm run dev # → http://localhost:3410TypeScript by default. Add --template javascript for plain JS.
npm run build # → dist/ (requires a local server)
npx actjs build --inline # → dist/index.html (single file, opens without a server)Requires Node.js 20+.
Or add to an existing project
npm install js-act// vite.config.ts
import { defineConfig } from 'vite';
import { actjsPlugin } from 'js-act/vite';
export default defineConfig({
plugins: [actjsPlugin()], // wires up JSX transform automatically
});// src/main.tsx
import { component, signal, createApp } from 'js-act';
const Counter = component(() => {
const [count, setCount] = signal(0);
return () => (
<div>
<p>Count: {count()}</p>
<button onclick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}, { hydrate: 'interactive' });
createApp('#root').mount(Counter);Or drop in a script tag (no build step)
<script src="https://cdn.jsdelivr.net/npm/js-act/dist/actjs.iife.js"></script>
<script>
const { component, signal, el, createApp } = actjs;
const Counter = component(() => {
const [count, setCount] = signal(0);
return () => el.div(null,
el.p(null, `Count: ${count()}`),
el.button({ onclick: () => setCount(c => c + 1) }, '+1'),
);
}, { hydrate: 'interactive' });
createApp('#root').mount(Counter);
</script>Why js-act?
| | js-act | React | Vue | Svelte | |---|---|---|---|---| | Gzipped size | 8 kB | 45 kB | 34 kB | 10 kB | | Runtime deps | 0 | 2 | 1 | 0 | | SSR built-in | ✅ | ❌ (needs Next) | ❌ (needs Nuxt) | ❌ (needs SvelteKit) | | Script tag | ✅ | ⚠️ | ⚠️ | ❌ | | Island hydration | ✅ | ❌ | ❌ | ⚠️ |
Key concepts
Signals — explicit, fine-grained reactivity
const [count, setCount] = signal(0); // read/write pair
const doubled = computed(() => count() * 2); // auto-tracks deps
// Reading inside a render fn = auto-subscribe to updates
return () => <p>{count()} × 2 = {doubled()}</p>;Components — setup once, render reactively
const MyComp = component((props) => {
// Setup runs ONCE
const [name, setName] = signal('');
onMount(() => { /* DOM ready */ });
onDestroy(() => { /* cleanup */ });
useInterval(() => tick(), 1000); // auto-clears on destroy
// Render fn re-runs when signals change
return () => <input value={name()} onInput={e => setName(e.target.value)} />;
}, { hydrate: 'interactive' });Island hydration — ship zero JS for static content
// Static (default) — renders to HTML, zero JS sent to browser
const Header = component(() => () => <header>...</header>);
// Interactive — hydrates immediately
const Counter = component(() => ..., { hydrate: 'interactive' });
// Visible — hydrates only when scrolled into view
const HeavyWidget = component(() => ..., { hydrate: 'visible' });Router
import { createRouter, navigate, Link, params } from 'js-act';
const App = createRouter([
{ path: '/', component: Home },
{ path: '/post/:slug', component: Post },
{ path: '*', component: NotFound },
]);
// Inside a component:
const { slug } = params(); // reactive — updates on route change
navigate('/post/hello');SSR
import { renderToString } from 'js-act/server';
const html = await renderToString(MyPage);
// → "<title>My Page</title><main>...</main>"CLI
actjs create <name> # scaffold a new project
actjs dev # dev server with HMR
actjs build # production build (multi-file, needs a server)
actjs build --inline # single self-contained index.html (double-click to open)
actjs preview # preview the build
actjs add <pkg>[@version] # add a CDN dependency
actjs remove <pkg> # remove a CDN dependencyContributing
Contributions are welcome! Here's how to get started:
git clone https://github.com/me-vaisakhr/actjs.git
cd js-act
npm install
npm test # run the test suite
npm run typecheck # TypeScript check
npm run build:all # build all outputs- All source lives in
src/— one file per module - Tests live in
tests/— one file per source module, 100% coverage enforced - Open an issue before starting large changes so we can align on approach
- PRs should include tests for any new behavior
License
MIT
