npm.io
0.0.36 • Published 2d ago

@mateosuarezdev/flash

Licence
MIT
Version
0.0.36
Deps
0
Size
200 kB
Vulns
0
Weekly
53

Flash

Fine-grained reactive JSX framework with zero VDOM overhead

Flash is a lightweight, performant JSX framework built on signals for fine-grained reactivity. Write familiar JSX, get blazing fast updates with direct DOM manipulation. First class enter-exit animations and rollbacks.


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 - onBeforeExit pauses 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-core

Configure 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 entry
  • replaceState - Replace current entry
  • popstate - Back/forward navigation
  • beforeunload - 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 resolved
renderToStream() - 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 updates
Pre-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 DOM
  • Fragment - Render multiple children without a wrapper
Lifecycle
  • onMount(callback) - Run after component mounts
  • onUnmount(callback) - Run when component unmounts
  • onBeforeExit(callback) - Run before unmounting (pauses unmount tree for animations, data saving, etc.)
Context
  • createContext(defaultValue) - Create a context
  • useContext(context) - Consume context value
Animations & Performance
  • startViewTransition(callback) - Trigger View Transition API
  • frame.read(callback) - Schedule DOM reads (measurements)
  • frame.update(callback) - Schedule calculations
  • frame.render(callback) - Schedule DOM writes (mutations)
  • frame.chain({ read, update, render }) - Chain operations with data flow
  • flip(element, applyChanges, options) - FLIP animation for single element
  • flipGroup(elements, applyChanges, options) - FLIP animation for groups
  • flipMove(element, newParent, options) - Animate element to new container
  • autoAnimate(element, options) - Enable auto layout animations (WIP)
Special Props
  • ref={(el) => ...} - Get reference to DOM element
  • key={value} - Unique identifier for list items
  • viewTransitionName={name} - Named view transition target
  • autoanimate={true} - Enable auto-layout animations (WIP)

Performance Tips

  1. Use signals at module level for shared state
  2. Wrap dynamic expressions in functions {() => signal.value} not {signal.value}
  3. Always use keys for list items
  4. Minimize reactive boundaries - only wrap what needs to update
  5. Use computed signals for derived state
  6. Use frame for DOM operations - Prevent layout thrashing by batching reads/writes
  7. Use FLIP for layout animations - Animate transforms instead of layout properties
  8. 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

Keywords