Skip to content

fictjs/fict

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

687 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fict Logo

Fict

Reactive UI with zero boilerplate.
Write JavaScript; let the compiler handle signals, derived values, and DOM updates.

CI npm version npm downloads license

Quick Start · Core Concepts · Examples · Docs · Playground


function Counter() {
  let count = $state(0)
  const doubled = count * 2 // auto-derived

  return <button onClick={() => count++}>{doubled}</button>
}

No useMemo. No dependency arrays. No .value. Just JavaScript.


Why Fict?

"Write JavaScript; the compiler handles reactivity." No .value, no deps arrays, no manual memo wiring. Not pitching "better React/Vue/Svelte" — Fict is a different mental model: compile-time reactivity on plain JS. The gain: less code, lower cognitive overhead.

Pain Point React Vue 3 Solid Svelte 5 Fict ✨
State syntax useState() + setter ref() + .value createSignal() + () $state() $state()
Derived values useMemo + deps computed() createMemo() $derived() automatic 🔥
Props destructure ⚠️ breaks reactivity ❌ breaks reactivity ✅ ($props())
Control flow native JS v-if/v-for <Show>/<For> {#if}/{#each} native JS

Fict gives you the best of every world:

  • 🧩 React's familiar syntax — JSX, destructuring-friendly, native if/for
  • Solid's fine-grained updates — no VDOM, surgical DOM patches
  • Less boilerplate than both — compiler infers derived values automatically

Quick Start

npm install fict
npm install -D @fictjs/vite-plugin  # Vite users
📦 Counter App — full example
import { $state, render } from 'fict'

export function Counter() {
  let count = $state(0)
  const doubled = count * 2 // auto-derived

  return (
    <div class="counter">
      <h1>Fict Counter</h1>
      <div class="card">
        <button onClick={() => count--}>-</button>
        <span class="count">{count}</span>
        <button onClick={() => count++}>+</button>
      </div>
      <p class="doubled">Doubled: {doubled}</p>
    </div>
  )
}

render(() => <Counter />, document.getElementById('app')!)
⚙️ Vite config
// vite.config.ts
import { defineConfig } from 'vite'
import fict from '@fictjs/vite-plugin'

export default defineConfig({
  plugins: [fict()],
})
🔧 TypeScript config
{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "fict"
  }
}

Online Examples


Core Concepts

$state — Reactive data

let count = $state(0)

count++ // ✅ direct mutation
count = count + 1 // ✅ assignment

Automatic derivations — No useMemo needed

let price = $state(100)
let quantity = $state(2)

const subtotal = price * quantity // auto-derived
const tax = subtotal * 0.1 // auto-derived
const total = subtotal + tax // auto-derived

The compiler builds a dependency graph and only recomputes what's needed. Single-use derived values may be inlined as an optimization; use $memo or set inlineDerivedMemos: false to force explicit memo nodes.

$effect — Side effects

$effect(() => {
  console.log(`count is now ${count}`)
  return () => {
    /* cleanup */
  }
})

Execution Model: Not React, Not Solid

This is the most important concept to understand.

function Counter() {
  console.log('A') // 🔵 Runs ONCE
  let count = $state(0)
  const doubled = count * 2
  console.log('B', doubled) // 🟢 Runs on EVERY count change
  return (
    <button onClick={() => count++}>
      {(console.log('C'), doubled)} {/* 🟢 Runs on every change */}
      {(console.log('D'), 'static')} {/* 🔵 Runs ONCE */}
    </button>
  )
}
Phase Output Why
Initial render A → B 0 → C → D Everything runs once
After click (count: 0→1) B 2 → C A and D don't run!

The mental model

Framework What happens on state change
React Entire component function re-runs
Solid Component runs once; you manually wrap derived values
Fict Component runs once; code depending on state auto-recomputes

Fict splits your component into reactive regions:

  • 🔵 Code before $state: runs once
  • 🟢 Expressions using state (count * 2): recompute when dependencies change
  • 🔵 Static JSX: runs once

Examples

Conditional rendering

function App() {
  let show = $state(true)

  return (
    <div>
      {show && <Modal />}
      {show ? <A /> : <B />}
    </div>
  )
}

No <Show> or {#if} — just JavaScript.

List rendering

function TodoList() {
  let todos = $state([
    { id: 1, text: 'Learn Fict' },
    { id: 2, text: 'Build something' },
  ])

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

No <For> or v-for — just .map().

Async data fetching

function UserProfile({ userId }: { userId: string }) {
  let user = $state<User | null>(null)
  let loading = $state(true)

  $effect(() => {
    const controller = new AbortController()
    loading = true

    fetch(`/api/user/${userId}`, { signal: controller.signal })
      .then(res => res.json())
      .then(data => {
        user = data
        loading = false
      })

    return () => controller.abort() // cleanup on userId change
  })

  if (loading) return <Spinner />
  return <div>{user?.name}</div>
}

Props stay reactive

function Greeting({ name, age = 18 }: { name: string; age?: number }) {
  const label = `${name} (${age})` // auto-derived from props
  return <span>{label}</span>
}

Destructuring works. No toRefs() or special handling needed.


What Fict Compiles To

// ✍️ Your code
function Counter() {
  let count = $state(0)
  const doubled = count * 2
  return <div>{doubled}</div>
}

// ⚡ Compiled output (simplified)
function Counter() {
  const count = createSignal(0)
  const doubled = createMemo(() => count() * 2)

  const div = document.createElement('div')
  createEffect(() => {
    div.textContent = doubled()
  })
  return div
}

You write the simple version. The compiler generates the efficient version.


Advanced Features

Error Boundaries

import { ErrorBoundary } from 'fict'
;<ErrorBoundary fallback={err => <p>Error: {String(err)}</p>}>
  <RiskyComponent />
</ErrorBoundary>

Suspense

import { Suspense } from 'fict'
import { resource, lazy } from 'fict/plus'

const userResource = resource({
  suspense: true,
  fetch: (_, id: number) => fetch(`/api/user/${id}`).then(r => r.json()),
})

const LazyChart = lazy(() => import('./Chart'))

function Profile({ id }) {
  return (
    <Suspense fallback="Loading...">
      <h1>{userResource.read(() => id).data?.name}</h1>
      <LazyChart />
    </Suspense>
  )
}

SSR Streaming

Fict SSR supports shell-first streaming with Suspense boundary patching:

import { renderToPipeableStream } from '@fictjs/ssr'

const { pipe, shellReady, allReady } = renderToPipeableStream(() => <App />, {
  mode: 'shell',
})

pipe(res)
await shellReady
await allReady
🧪 Partial prerendering (Preview)
import { renderToPartial } from '@fictjs/ssr'

const { shell, stream } = renderToPartial(() => <App />, { mode: 'shell' })
// shell: complete fallback HTML
// stream: deferred boundary patches

renderToPartial is an advanced API (Preview in v1.0).

fict/plus — Advanced APIs

import { $store, untrack } from 'fict'
import { resource, lazy } from 'fict/plus'

// Deep reactivity with path-level tracking
const user = $store({ name: 'Alice', address: { city: 'London' } })
user.address.city = 'Paris' // fine-grained update

// Derived values are auto-memoized, just like $state
const greeting = `Hello, ${user.name}` // auto-derived

// Method chains are also auto-memoized
const store = $store({ items: [1, 2, 3, 4, 5] })
const doubled = store.items.filter(n => n > 2).map(n => n * 2) // auto-memoized

// Dynamic property access works with runtime tracking
const value = store[props.key] // reactive, updates when key or store changes

// Escape hatch for black-box functions
const result = untrack(() => externalLib.compute(count))
📊 $store vs $state
Feature $state $store
Depth Shallow Deep (nested objects)
Access Direct value Proxy-based
Mutations Reassignment Direct property mutation
Derived values Auto-memoized Auto-memoized
Best for Primitives, simple objects Complex nested state

Control Flow and Branch Reactivity

Fict components execute once on mount. Reactive updates happen through bindings/memos.

JSX-only reads → fine-grained DOM updates:

let count = $state(0)
return <div>{count}</div> // Only the text node updates

Control flow returns → compiler emits reactive branch bindings:

let count = $state(0)
if (count > 10) return <Special /> // branch swaps reactively when count changes
return <Normal />

The compiler detects supported patterns (if-return, switch-return, try blocks containing return branches) and lowers them to reactive conditionals.


Framework Comparison

Feature React+Compiler Solid Svelte 5 Vue 3 Fict ✨
State syntax useState() createSignal() $state() ref() $state()
Read state count count() count count.value count
Update state setCount(n) setCount(n) count = n count.value = n count = n
Derived values auto createMemo() $derived() computed() auto
Props destructure via $props() via toRefs()
Control flow native JS <Show>/<For> {#if}/{#each} v-if/v-for native JS
File format .jsx/.tsx .jsx/.tsx .svelte .vue .jsx/.tsx
Rendering VDOM fine-grained fine-grained fine-grained fine-grained

Performance

🚧 Note: Bundle size and memory optimizations are currently in progress.

Performance Benchmark

Benchmark Summary (js-framework-benchmark)

Benchmark Vue Vapor Solid Svelte 5 Fict React Compiler
create rows (1k) 24.5ms 24.5 24.5 26.2 29.3
replace all rows (1k) 28.1 28.0 29.1 30.7 34.8
partial update (10th row) 14.7 15.0 15.3 15.3 18.6
select row 3.4 4.2 6.2 3.6 10.3
swap rows 17.4 17.7 17.2 17.1 115.6
remove row 11.5 11.4 11.8 12.0 13.9
create many rows (10k) 263.7 256.8 264.6 270.7 398.2
append rows (1k to 1k) 29.7 29.0 29.2 30.4 35.5
clear rows (1k) 11.8 15.1 13.5 14.3 21.6
Geometric Mean 1.01 1.04 1.06 1.07 1.45

Lower is better. Geometric mean is the weighted mean of all relative factors.

Versions: Vue Vapor 3.6.0-alpha.2 · Solid 1.9.3 · Svelte 5.42.1 · React Compiler 19.0.0


Status & Roadmap

⚠️ Alpha — Fict is feature-complete for core compiler and runtime. API is stable, but edge cases may be refined. Don't use it in production yet.

✅ Completed

  • Compiler with HIR/SSA
  • Stable $state / $effect semantics
  • Automatic derived value inference
  • $store in fict, resource/lazy in fict/plus
  • startTransition, useTransition, useDeferredValue in fict
  • Vite plugin
  • ESLint plugin
  • Support sourcemap
  • DevTools
  • Router
  • Testing library
  • SSR / streaming

🗺️ Planned

  • Migration guides from React/Vue/Svelte/Solid

Documentation

Doc Description
Architecture How the compiler and runtime work
API Reference Complete API documentation
Compiler Spec Formal semantics
ESLint Rules Linting configuration
Diagnostic Codes Compiler warnings reference
Config Profiles Recommended dev/CI/prod settings
Cycle Protection Dev-mode infinite loop detection
SSR SEO Guide SEO best practices for SSR pages
SSR Performance Snapshot size & render-mode tuning
SSR Deployment Vercel/Cloudflare/edge deployment
DevTools Vite plugin usage & auto-injection
🔍 Linting & diagnostics

Install @fictjs/eslint-plugin and extend plugin:fict/recommended:

{
  "plugins": ["fict"],
  "extends": ["plugin:fict/recommended"]
}

Key rules: nested component definitions (FICT-C003), missing list keys (FICT-J002), memo side effects (FICT-M003), empty $effect (FICT-E001), component return checks (FICT-C004), plus $state placement/alias footguns.

  • Recommended config mirrors compiler warnings so IDE diagnostics stay aligned with build output.
  • For strict CI gates, enable compiler strictReactivity: true to escalate control-flow fallback diagnostics (FICT-R003, FICT-R006) to build errors.
  • strictGuarantee is enabled by default for fail-closed guarantees.
  • Set strictGuarantee: false only when you intentionally opt out.
  • CI can force strict mode with FICT_STRICT_GUARANTEE=1 during build steps.
  • Guarantee boundary reference: docs/reactivity-guarantee-matrix.md.

FAQ

Is Fict production-ready?

Alpha. Core is stable, but expect edge cases. Test thoroughly for critical apps.

Does Fict use a virtual DOM?

No. Fict compiles to direct DOM operations for surgical, fine-grained updates.

How does Fict handle arrays?

Default: immutable style (todos = [...todos, newTodo]). For deep mutations, use spread to create new immutable data, or use Immer/Mutative, or use $store from fict.

Can I use existing React components?

Not directly. Fict compiles to DOM operations, not React elements.

How big is the runtime?

~10kb brotli compressed. Performance is within ~3% of Solid in js-framework-benchmark (geometric mean 1.07 vs 1.04).


Known Limitations

The compiler has some limitations when handling conditional rendering patterns.

Control-flow patterns supported

  • Multiple sequential if-return branches are compiled into reactive conditionals.
  • if blocks without return are auto-wrapped so reactive side effects still update.
  • Nested branch logic (e.g. inner if/switch) and reactive prelude reads before return are automatically kept reactive. When fine-grained lowering is not possible, the compiler enables a safe runtime fallback that tracks branch reads and re-runs the active branch.

Acknowledgments

Fict is built upon the brilliant ideas and relentless innovation of the open-source community. We express our deepest respect and gratitude to these projects:

  • React — For defining the modern era of UI development. Its component model and declarative philosophy set the standard for developer experience.
  • Solid — For pioneering fine-grained reactivity and demonstrating the power of compilation. Its architecture is the bedrock upon which Fict's performance is built.
  • Qwik — For its outstanding resumability-first SSR vision. Its approach to instant interactivity has been a major inspiration for Fict's resumable SSR direction.
  • alien-signals — For pushing the boundaries of signal performance. Its implementation provided critical guidance for Fict's reactive system.

MIT License · © Fict Contributors

About

(WIP)Reactive UI with zero boilerplate

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors