Note: This is an experiment in iterative AI-assisted development — a "see if I could" project more than anything production-grade. The goal was to build something resembling pre-hooks React but without the lifecycle method mess: just pure render functions, external stores, and a clean connect API. Every commit was planned and written collaboratively with Claude Code.
A render-driven UI framework with virtual DOM and immutable stores. Like React, but with no hooks — state stores are first-class citizens and components are pure render functions.
- No hooks. All state lives in external stores. Components are
(props) => VNode. - Stores are first-class. Create, import, and share stores anywhere. They're framework-agnostic. Per-component local stores for UI-only state.
- Render-driven. Describe what the UI looks like for a given state. Pulse handles the rest.
- Built-in routing. Store-based client-side router — routes are just state.
- Middleware. Pluggable middleware for logging, action history, and custom logic.
- Devtools. Built-in browser devtools panel — store inspector, action replay, time-travel.
- Tiny. ~7 KB gzipped core, ~9 KB devtools. Zero runtime dependencies.
npm install @shane_il/pulseConfigure JSX automatic runtime (Vite, TypeScript, or Babel):
Vite (vite.config.js):
export default defineConfig({
esbuild: {
jsx: 'automatic',
jsxImportSource: '@shane_il/pulse',
},
});TypeScript (tsconfig.json / jsconfig.json):
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@shane_il/pulse"
}
}No need to import h in every file — the bundler handles it automatically.
Classic mode (manual h import)
{ "compilerOptions": { "jsx": "react", "jsxFactory": "h", "jsxFragmentFactory": "Fragment" } }Then import { h } from '@shane_il/pulse' in every JSX file.
import { createStore, connect, render } from '@shane_il/pulse';
// 1. Create a store
const counterStore = createStore({
state: { count: 0 },
actions: {
increment: (s) => ({ count: s.count + 1 }),
decrement: (s) => ({ count: s.count - 1 }),
},
});
// 2. Write a pure component
function Counter({ count }) {
return (
<div>
<h1>{count}</h1>
<button onClick={() => counterStore.actions.increment()}>+</button>
<button onClick={() => counterStore.actions.decrement()}>-</button>
</div>
);
}
// 3. Connect it to the store
const ConnectedCounter = connect.from(counterStore, 'count')(Counter);
// or: connect({ count: counterStore.select(s => s.count) })(Counter)
// 4. Render (defaults to #app)
render(<ConnectedCounter />);Store dispatch → Notify subscribers → Schedule re-render → Expand components
→ Diff VDOM → Patch DOM (single paint)
- Stores hold immutable state. Actions produce new state via pure functions.
connect()subscribes components to store slices via selectors.- When a store changes, connected components whose selected values differ are scheduled for re-render.
- The scheduler batches multiple store updates in the same tick into a single render pass.
- The VDOM engine diffs the old and new virtual trees and patches only the changed DOM nodes.
- Todo App — stores, connected components, routing, devtools
- Weather Dashboard — async actions, multi-store connect, logger middleware
- Getting Started — installation, JSX setup, first app, project structure
- Stores —
createStore, actions, selectors, subscriptions, derived state - Components — pure components,
connect(), keyed lists, error boundaries - Lifecycle —
onMount,onUpdate,onDestroy,onError, cleanup functions - Routing —
createRouter, Route/Link/Redirect, path matching, nested routes - Middleware —
logger,actionHistory,createAsyncAction, custom middleware - Devtools — browser panel, store inspector, time-travel, component tracking
- Architecture — how the VDOM engine works under the hood
npm install
npm test # 313 tests (vitest)
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run build # vite lib mode → dist/MIT