@mateosuarezdev/flash
v0.0.22
Published
Custom jsx runtime
Maintainers
Readme
⚡ Flash
Fine-grained reactive JSX framework with zero VDOM overhead
Flash is a lightweight, performant JSX framework built on Preact Signals for fine-grained reactivity. Write familiar JSX, get blazing fast updates with direct DOM manipulation.
Features
Client-Side
- ⚡ Fine-grained reactivity - Powered by Preact Signals, updates only what changed
- 🎯 No Virtual DOM - Direct DOM manipulation for maximum performance
- 🎭 Flexible animations - Use any library (Framer Motion, GSAP, etc.) or vanilla CSS/classes
- 🎪 DOM resurrection - Automatic animation reversal for rapid toggling
- 🔑 Keyed list rendering - SolidJS-style efficient list updates (insert/update/delete/move)
- 🎨 View Transitions API - Smooth page transitions out of the box
- 🏎️ Frame scheduler - Prevent layout thrashing with read/update/render phases
- 🎬 FLIP animations - Built-in utilities for performant layout animations
- 🪄 Auto-animate - Automatic layout animations (in progress)
- 🪝 First-class exit animations -
onBeforeExitpauses unmounting (save data, animations, etc.) - 🌳 Context API - Share state across component trees
- 🧭 Built-in Router - File-based routing with lazy loading (work in progress)
- 📦 Tiny bundle size - No compiler required, minimal runtime
- 🔄 Reactive props - Props can be signals for automatic updates
Server-Side
- 🌐 Server-Side Rendering - Three rendering strategies (sync, async, streaming)
- ⚡ Parallel async resolution - Resolves async components efficiently
- 📦 Pre-rendering & caching - Built-in static site generation
- 🌊 Progressive enhancement - Stream HTML for better perceived performance
- 🔒 Automatic XSS protection - HTML escaping by default
Installation
npm install @mateosuarezdev/flash @preact/signals-coreConfigure your tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@mateosuarezdev/flash/runtime"
}
}Quick Start
import { render, onMount } from "@mateosuarezdev/flash";
import { signal } from "@preact/signals-core";
const count = signal(0);
function Counter() {
onMount(() => {
console.log("Counter mounted!");
});
return (
<div>
<h1>Count: {() => count.value}</h1>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
render(<Counter />, document.getElementById("app"));Core Concepts
Reactive Boundaries
Wrap expressions in functions to create reactive boundaries that update automatically when signals change:
import { signal } from "@preact/signals-core";
const name = signal("World");
function Greeting() {
return (
<div>
{/* This updates when name changes */}
<h1>Hello {() => name.value}!</h1>
{/* Conditional rendering */}
{() =>
name.value === "World" ? (
<p>Welcome!</p>
) : (
<p>Hello, {() => name.value}!</p>
)
}
</div>
);
}Reactive Props
Props can be functions that automatically update:
const isDark = signal(false)
<button
className={() => isDark.value ? 'dark' : 'light'}
disabled={() => !isDark.value}
>
Toggle
</button>Keyed Lists
Use the key prop for efficient list rendering:
const items = signal([
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Cherry" },
]);
function List() {
return (
<ul>
{() => items.value.map((item) => <li key={item.id}>{item.name}</li>)}
</ul>
);
}Flash efficiently handles:
- ✅ Inserts - New items are rendered and inserted
- ✅ Removals - Deleted items are unmounted and removed
- ✅ Reordering - DOM nodes are moved to match new order
- ✅ Updates - Existing items are reused (no re-render)
Lifecycle Hooks
onMount
Runs after the component is mounted to the DOM:
function Component() {
let ref: HTMLDivElement;
onMount(() => {
console.log("Element:", ref);
// Fetch data, start animations, etc.
});
return <div ref={(el) => (ref = el)}>Content</div>;
}onUnmount
Runs when the component is removed from the DOM:
function Component() {
onUnmount(() => {
console.log("Cleaning up...");
// Cancel subscriptions, clear timers, etc.
});
return <div>Content</div>;
}onBeforeExit
Runs before unmounting - a first-class feature, not a workaround. You can pause the entire unmount process to:
- Play exit animations
- Save form data or state
- Confirm user actions
- Clean up async operations
- Anything you need before removal
The unmount tree is held until your async callback completes:
import { animate } from "framer-motion";
function FadeBox() {
let ref: HTMLElement;
onBeforeExit(async (token) => {
// Play exit animation
const animation = animate(ref, { opacity: 0 }, { duration: 0.3 });
// Handle cancellation (e.g., rapid toggling)
token.onCancel(() => {
animation.stop();
animate(ref, { opacity: 1 }, { duration: 0.3 });
});
await animation.finished;
// Component won't unmount until animation completes
});
return <div ref={(el) => (ref = el)}>Fading content</div>;
}Save data before unmounting:
function Form() {
const formData = signal({ name: "", email: "" });
onBeforeExit(async () => {
// Save to localStorage or API
await saveFormData(formData.value);
console.log("Data saved before unmount!");
});
return <form>...</form>;
}Cancellation Example:
const show = signal(true)
// Rapid toggling: true → false → true
// Flash will cancel the exit animation and reuse the DOM!
<button onClick={() => show.value = !show.value}>
Toggle
</button>
{() => show.value && <FadeBox />}Context API
Share state across component trees without prop drilling:
import { createContext, useContext } from "@mateosuarezdev/flash";
const ThemeContext = createContext({ theme: "light" });
function App() {
ThemeContext.provide({ theme: "dark" });
return <Child />;
}
function Child() {
const theme = useContext(ThemeContext);
console.log(theme); // { theme: 'dark' }
return <div>Theme: {theme.theme}</div>;
}Router (Work in Progress)
Flash includes a built-in router with reactive pathname tracking and custom URL change events:
import { Router, pathname, push, replace, back, onUrlChange } from '@mateosuarezdev/flash/router'
function App() {
return (
<Router>
<nav>
<a href="/" onClick={(e) => { e.preventDefault(); push('/') }}>Home</a>
<a href="/about" onClick={(e) => { e.preventDefault(); push('/about') }}>About</a>
</nav>
{/* Reactive routing based on pathname signal */}
{() => {
switch (pathname.value) {
case '/':
return <Home />
case '/about':
return <About />
default:
return <NotFound />
}
}}
</Router>
)
}Core Router Features
Reactive Pathname Tracking:
import { pathname } from '@mateosuarezdev/flash/router'
// pathname is a signal that updates on navigation
function NavBar() {
return (
<nav>
<a
class={() => pathname.value === '/' ? 'active' : ''}
href="/"
>
Home
</a>
</nav>
)
}Programmatic Navigation:
import { push, replace, back } from '@mateosuarezdev/flash/router'
// Navigate to a new route
push('/dashboard')
// Replace current route (no history entry)
replace('/login')
// Go back in history
back()URL Change Events:
import { onUrlChange } from '@mateosuarezdev/flash/router'
function Component() {
onUrlChange((event) => {
if (event) {
console.log('Navigation:', event.action) // 'pushState' | 'replaceState' | 'popstate' | 'beforeunload'
console.log('From:', event.oldURL?.pathname)
console.log('To:', event.newURL?.pathname)
// Prevent navigation by calling event.preventDefault()
// Great for unsaved changes warnings
}
})
return <div>Content</div>
}Custom UrlChangeEvent:
The router automatically intercepts and dispatches custom urlchangeevent for all navigation:
pushState- New history entryreplaceState- Replace current entrypopstate- Back/forward navigationbeforeunload- Page close/reload
Events can be prevented to block navigation (useful for form guards, unsaved changes, etc.)
Current Features:
- ✅ Reactive pathname signal
- ✅ Programmatic navigation (push, replace, back)
- ✅ Custom URL change events with prevention
- ✅ History state tracking
- ✅ Lifecycle integration (auto-cleanup with onUnmount)
Planned Features:
- 📁 File-based routing with automatic route generation
- 🔀 Lazy loading and code splitting helpers
- 🎨 Integrated View Transitions API support
- 📱 Nested routes and layouts
- 🎯 Route guards and middleware
- 🔍 Path parameter extraction
- 🔗 Link component with active state
Note: The router is currently in active development. The current implementation provides low-level primitives for building routing solutions!
Animations & Performance
Flash is designed to be a batteries-included framework with powerful animation and performance utilities built right in.
Flexible Animation Options
Flash gives you complete freedom to animate however you want:
1. Use Any Animation Library
import { animate } from 'framer-motion'
import { animate as animateJsAnimate } from 'animejs'
function Component() {
let ref: HTMLElement
onMount(() => {
// Framer Motion
animate(ref, { x: 100 }, { duration: 0.3 })
// Anime.js, GSAP, Motion One, or any library!
})
onBeforeExit(async (token) => {
const animation = animate(ref, { opacity: 0 }, { duration: 0.3 })
token.onCancel(() => {
animation.stop()
animate(ref, { opacity: 1 }, { duration: 0.3 })
})
await animation.finished
})
return <div ref={(el) => ref = el}>Animated</div>
}2. Vanilla CSS Transitions
function Component() {
let ref: HTMLElement
onMount(() => {
ref.style.transition = 'opacity 300ms'
ref.style.opacity = '0'
setTimeout(() => ref.style.opacity = '1', 10)
})
onBeforeExit(async () => {
ref.style.opacity = '0'
await new Promise(resolve => setTimeout(resolve, 300))
})
return <div ref={(el) => ref = el}>CSS Animated</div>
}3. Toggle CSS Classes
function Component() {
let ref: HTMLElement
onMount(() => {
requestAnimationFrame(() => {
ref.classList.add('enter-active')
setTimeout(() => ref.classList.remove('enter-active'), 300)
})
})
onBeforeExit(async () => {
ref.classList.add('exit-active')
await new Promise(resolve => setTimeout(resolve, 300))
})
return <div ref={(el) => ref = el} class="animated">Content</div>
}Built-in Performance Utilities
Frame Scheduler (Prevent Layout Thrashing)
Inspired by Framer Motion's frame loop, Flash includes a high-performance scheduler that prevents layout thrashing by separating read/update/render phases:
import { frame } from '@mateosuarezdev/flash'
// Simple usage
frame.read(() => {
const height = element.offsetHeight // DOM reads
})
frame.update(() => {
position += velocity // Calculations
})
frame.render(() => {
element.style.transform = `translateY(${position}px)` // DOM writes
})
// Chained operations with type-safe data flow
frame.chain({
read: () => element.offsetHeight,
update: (height) => height * 2,
render: (doubled) => element.style.height = `${doubled}px`
})
// Keep-alive for continuous animations
const animate = frame.render(() => {
element.style.transform = `rotate(${rotation}deg)`
}, true) // true = runs every frame
// Cancel when done
frame.cancel(animate)FLIP Animations
Built-in FLIP (First, Last, Invert, Play) utilities for performant layout animations:
import { flip, flipGroup } from '@mateosuarezdev/flash'
// Animate a single element
flip(element, () => {
// Make DOM changes
element.classList.add('expanded')
element.style.width = '400px'
}, {
duration: 300,
easing: 'ease-out-cubic'
})
// Animate list reordering
const items = document.querySelectorAll('.item')
flipGroup(items, () => {
// Reorder items
container.appendChild(items[2])
}, {
duration: 400,
easing: 'ease-out-cubic'
})Auto-Animate (Work in Progress)
Automatically animate layout changes (inspired by Framer Motion's layout animations):
<div autoanimate>
{/* Children automatically animate when added/removed/reordered */}
{() => items.value.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>Note: Auto-animate is currently in development and will provide automatic FLIP animations for layout changes without manual setup.
View Transitions API
Built-in support for the browser's View Transitions API:
import { startViewTransition } from '@mateosuarezdev/flash'
const expanded = signal(false)
<button onClick={() => {
startViewTransition(() => {
expanded.value = !expanded.value
})
}}>
Toggle
</button>
<div
className={() => expanded.value ? 'expanded' : 'collapsed'}
viewTransitionName="container"
>
Content
</div>Performance Best Practices
Use the Frame Scheduler:
// ❌ Bad: Layout thrashing
const height = element.offsetHeight // Read
element.style.height = `${height * 2}px` // Write
const width = element.offsetWidth // Read (forces reflow!)
element.style.width = `${width * 2}px` // Write
// ✅ Good: Batched reads and writes
frame.chain({
read: () => ({
height: element.offsetHeight,
width: element.offsetWidth
}),
render: ({ height, width }) => {
element.style.height = `${height * 2}px`
element.style.width = `${width * 2}px`
}
})Use FLIP for Layout Changes:
// ❌ Bad: Animating layout properties directly
element.animate({ width: '400px', height: '300px' }, { duration: 300 })
// ✅ Good: Use FLIP to transform instead
flip(element, () => {
element.style.width = '400px'
element.style.height = '300px'
}, { duration: 300 })Advanced Examples
Counter with Computed Values
import { signal, computed } from "@preact/signals-core";
const count = signal(0);
const double = computed(() => count.value * 2);
function Counter() {
return (
<div>
<p>Count: {() => count.value}</p>
<p>Double: {() => double.value}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}Dynamic List with Add/Remove
import { signal } from "@preact/signals-core";
const items = signal([
{ id: 1, name: "Task 1" },
{ id: 2, name: "Task 2" },
]);
let nextId = 3;
function TodoList() {
const addItem = () => {
items.value = [...items.value, { id: nextId++, name: `Task ${nextId}` }];
};
const removeItem = (id: number) => {
items.value = items.value.filter((item) => item.id !== id);
};
return (
<div>
<button onClick={addItem}>Add Task</button>
<ul>
{() =>
items.value.map((item) => (
<li key={item.id}>
{item.name}
<button onClick={() => removeItem(item.id)}>Delete</button>
</li>
))
}
</ul>
</div>
);
}Nested Reactive Updates
const user = signal({ name: "John", age: 25 });
function Profile() {
return (
<div>
<h1>{() => user.value.name}</h1>
<p>Age: {() => user.value.age}</p>
<button
onClick={() => {
user.value = { ...user.value, age: user.value.age + 1 };
}}
>
Birthday
</button>
</div>
);
}Conditional Rendering with Animations
import { animate } from "framer-motion";
const show = signal(true);
function AnimatedBox() {
let ref: HTMLElement;
onMount(() => {
animate(ref, { opacity: [0, 1], y: [-20, 0] }, { duration: 0.3 });
});
onBeforeExit(async (token) => {
const animation = animate(ref, { opacity: 0, y: -20 }, { duration: 0.3 });
token.onCancel(() => {
animation.stop();
animate(ref, { opacity: 1, y: 0 }, { duration: 0.3 });
});
await animation.finished;
});
return <div ref={(el) => (ref = el)}>Animated content</div>;
}
function App() {
return (
<div>
<button onClick={() => (show.value = !show.value)}>Toggle</button>
{() => show.value && <AnimatedBox />}
</div>
);
}Server-Side Rendering
Flash provides three rendering strategies for different use cases:
renderToString() - Synchronous
Fast synchronous rendering for static content:
import { renderToString } from '@mateosuarezdev/flash/server'
const html = renderToString(<App />)
// Returns: Complete HTML string (no async support)renderToStringAsync() - Complete HTML
Waits for all async components, perfect for SEO and pre-rendering:
import { renderToStringAsync } from '@mateosuarezdev/flash/server'
const html = await renderToStringAsync(<App />)
// Returns: Complete HTML with all async resolvedrenderToStream() - Progressive Enhancement
Stream HTML for better perceived performance:
import { renderToStream } from '@mateosuarezdev/flash/server'
const stream = renderToStream(<App />)
for await (const chunk of stream) {
response.write(chunk)
}
// Streams: Initial HTML + progressive updatesPre-rendering & Caching
import { prerenderer } from '@mateosuarezdev/flash/server/prerender'
// Save pre-rendered HTML
await prerenderer.save('/', html)
// Load from cache
const cached = await prerenderer.load('/')
if (cached) return new Response(cached)Learn more: Check out the Server Architecture Guide for detailed information about:
- Rendering strategies comparison
- Async component resolution
- Streaming architecture
- Caching and pre-rendering
- Security best practices
API Reference
Core
render(element, container)- Mount your app to the DOMFragment- Render multiple children without a wrapper
Lifecycle
onMount(callback)- Run after component mountsonUnmount(callback)- Run when component unmountsonBeforeExit(callback)- Run before unmounting (pauses unmount tree for animations, data saving, etc.)
Context
createContext(defaultValue)- Create a contextuseContext(context)- Consume context value
Animations & Performance
startViewTransition(callback)- Trigger View Transition APIframe.read(callback)- Schedule DOM reads (measurements)frame.update(callback)- Schedule calculationsframe.render(callback)- Schedule DOM writes (mutations)frame.chain({ read, update, render })- Chain operations with data flowflip(element, applyChanges, options)- FLIP animation for single elementflipGroup(elements, applyChanges, options)- FLIP animation for groupsflipMove(element, newParent, options)- Animate element to new containerautoAnimate(element, options)- Enable auto layout animations (WIP)
Special Props
ref={(el) => ...}- Get reference to DOM elementkey={value}- Unique identifier for list itemsviewTransitionName={name}- Named view transition targetautoanimate={true}- Enable auto-layout animations (WIP)
Performance Tips
- Use signals at module level for shared state
- Wrap dynamic expressions in functions
{() => signal.value}not{signal.value} - Always use keys for list items
- Minimize reactive boundaries - only wrap what needs to update
- Use computed signals for derived state
- Use
framefor DOM operations - Prevent layout thrashing by batching reads/writes - Use FLIP for layout animations - Animate transforms instead of layout properties
- Leverage DOM resurrection - Flash automatically reuses DOM for rapid toggles
Comparison to Other Frameworks
| Feature | Flash | React | SolidJS | Vue | | ------------------------ | ------- | ----- | ------- | ------- | | Reactivity | Signals | VDOM | Signals | Proxies | | Bundle Size | ~10KB | ~40KB | ~7KB | ~30KB | | Fine-grained Updates | ✅ | ❌ | ✅ | ✅ | | Keyed Lists | ✅ | ✅ | ✅ | ✅ | | Built-in FLIP Utils | ✅ | ❌ | ❌ | ❌ | | Frame Scheduler | ✅ | ❌ | ❌ | ❌ | | DOM Resurrection | ✅ | ❌ | ❌ | ❌ | | Animation Flexibility | Any lib | Any lib | Any lib | Any lib | | SSR Support | ✅ | ✅ | ✅ | ✅ | | SSR Streaming | ✅ | ✅ | ✅ | ✅ | | No Compiler | ✅ | ✅ | ❌ | ❌ |
FAQ
Q: Do I need a compiler?
A: No! Flash works with standard JSX transformation. Just configure jsxImportSource.
Q: Can I use TypeScript? A: Yes! Flash is written in TypeScript with full type support.
Q: How does reactivity work?
A: Flash uses Preact Signals. When you wrap an expression in a function {() => signal.value}, Flash creates a reactive boundary that auto-updates when the signal changes.
Q: What about SSR? A: Yes! Flash has full SSR support with three rendering strategies (sync, async, streaming). See the Server Architecture Guide.
Q: Why functions for reactive values? A: Functions create clear boundaries for reactivity and work without a compiler. It's explicit and simple.
Q: What is DOM resurrection? A: When a component is exiting (playing exit animation) but gets toggled back on, Flash cancels the animation and reuses the existing DOM instead of creating a new one. This provides smooth animation reversals without any setup.
Examples
Check out the examples directory for more demos:
- Basic counter and computed values
- Enter/exit animations with cancellation
- View Transitions API integration
- Keyed list rendering with add/remove
- Context API usage
- Reactive props and class names
Architecture
Want to understand how Flash works under the hood?
Client Architecture Guide - Deep dive into:
- JSX transformation flow
- VNode types and rendering pipeline
- Reactivity system internals
- Content replacement strategies (resurrection, text optimization)
- Keyed list reconciliation algorithm
Server Architecture Guide - Deep dive into:
- Three rendering strategies (sync, async, streaming)
- Async component resolution (parallel execution)
- Streaming architecture and progressive enhancement
- Pre-rendering and caching system
- Security best practices (XSS protection)
Contributing
Flash is in active development. Contributions are welcome!
License
MIT © Mateo Suarez
⚡ Built with Flash - Fine-grained reactivity meets familiar JSX
