npm package: @mateosuarezdev/flash
Description: Custom jsx runtime
Author: Mateo Suarez
Latest version: 0.0.22

# ⚡ 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** - `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

```bash
npm install @mateosuarezdev/flash @preact/signals-core
```

Configure your `tsconfig.json`:

```json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@mateosuarezdev/flash/runtime"
  }
}
```

---

## Quick Start

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:**

```tsx
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:**

```tsx
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:

```tsx
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:

```tsx
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:**
```tsx
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:**
```tsx
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:**
```tsx
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

```tsx
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

```tsx
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

```tsx
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:

```tsx
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:

```tsx
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):

```tsx
<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:

```tsx
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:**
```tsx
// ❌ 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:**
```tsx
// ❌ 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

```tsx
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

```tsx
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

```tsx
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

```tsx
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:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
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](./docs/server-architecture.md) 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](./docs/server-architecture.md).

**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](./src/main.tsx) 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](./docs/architecture.md)** - 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](./docs/server-architecture.md)** - 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**