Reactive UI with zero boilerplate.
Fict is a UI library where you write plain JavaScript and the compiler figures out the reactivity.
Write JavaScript; let the compiler handle signals, derived values, and DOM updates. It’s a new way to think about UI—not a drop-in replacement for React/Vue/Svelte. The promise is less code and lower cognitive load.
function Counter() {
let count = $state(0)
const doubled = count * 2 // auto-derived, no useMemo needed
return <button onClick={() => count++}>{doubled}</button>
}No useMemo. No dependency arrays. No .value. Just JavaScript.
Positioning
- “Write JavaScript; the compiler handles reactivity.” No
.value, no deps arrays, no manual memo wiring (no explicit unwrap/getter calls). - 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. Performance is surgical by design, but we’re not selling unproven speed charts.
| Pain Point | React | Vue 3 | Solid | Svelte 5 | Fict |
|---|---|---|---|---|---|
| State syntax | useState() + setter |
ref() + .value (JS) / template auto-unwrap / or reactive() |
createSignal() + () calls |
$state() |
$state() |
| Derived values | useMemo + deps (or Compiler) |
computed() |
createMemo() |
$derived() |
automatic |
| Props destructure | ✅ | <script setup> / Vue 3.5+ defineProps destructure OK) |
❌ (breaks reactivity) | ✅ ($props() semantics) |
✅ |
| Control flow | native JS | template: v-if/v-for; render/JSX: native JS |
typically <Show>/<For> |
{#if}/{#each} |
native JS |
Fict gives you:
- React's familiar syntax — JSX, destructuring-friendly, native
if/for, etc. - Solid's fine-grained update model — no VDOM, surgical DOM updates
- Less boilerplate than both — compiler infers derived values automatically (when possible)
npm install fict
npm install -D @fictjs/vite-plugin # Vite usersCounter App:
import { $state, render } from 'fict'
export function Counter() {
let count = $state(0)
const doubled = count * 2 // auto-derived, no useMemo needed
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 setup:
// vite.config.ts
import { defineConfig } from 'vite'
import fict from '@fictjs/vite-plugin'
export default defineConfig({
plugins: [fict()],
})TypeScript:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "fict"
}
}let count = $state(0)
count++ // ✅ direct mutation
count = count + 1 // ✅ assignmentlet 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-derivedThe compiler builds a dependency graph and only recomputes what's needed.
$effect(() => {
console.log('count is now', count)
return () => {
/* cleanup */
}
})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>
)
}Initial render: A → B 0 → C → D
After click (count: 0 → 1): B 2 → C (A and D don't run!)
| 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
function App() {
let show = $state(true)
return (
<div>
{show && <Modal />}
{show ? <A /> : <B />}
</div>
)
}No <Show> or {#if} — just JavaScript.
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().
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>
}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.
// Your code
function Counter() {
let count = $state(0)
const doubled = count * 2
return <div>{doubled}</div>
}
// Compiled output (simplified)
function Counter() {
const [count, setCount] = 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.
import { ErrorBoundary } from 'fict'
;<ErrorBoundary fallback={err => <p>Error: {String(err)}</p>}>
<RiskyComponent />
</ErrorBoundary>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>
)
}import { $store, resource, lazy, untrack } 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 |
When does a component re-execute vs just update DOM?
JSX-only reads → Fine-grained DOM updates:
let count = $state(0)
return <div>{count}</div> // Only the text node updatesControl flow reads → Component re-executes:
let count = $state(0)
if (count > 10) return <Special /> // Component re-runs when count changes
return <Normal />The compiler detects this automatically. You don't need to think about it — write natural if/for and Fict does the right thing.
| 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 |
🚧 Note: Bundle size and memory optimizations are currently in progress.
| Metric | Vue Vapor | Solid | Svelte 5 | Fict | React Compiler |
|---|---|---|---|---|---|
| CPU (Duration) | 1.01 | 1.04 | 1.05 | 1.09 | 1.45 |
| Memory | 1.24 | 1.00 | 1.20 | 1.22 | 2.08 |
| Size / Load | 2.62 | 1.00 | 2.23 | 2.23 | 9.65 |
Lower is better. Baseline relative to best performer in each category.
⚠️ 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.
- Compiler with HIR/SSA
- Stable
$state/$effectsemantics - Automatic derived value inference
-
$store,resource,lazy,transitioninfict/plus - Vite plugin
- ESLint plugin
- Support sourcemap
- DevTools
- SSR / streaming
- TypeScript language service plugin
- Migration guides from React/Vue/Svelte/Solid
- Router
- Testing library
- Architecture — How the compiler and runtime work
- Compiler Spec — Formal semantics
- ESLint Rules — Linting configuration
- Diagnostic Codes — Compiler warnings reference
- Install
@fictjs/eslint-pluginand extendplugin:fict/recommendedto mirror compiler guardrails. - 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$stateplacement/alias footguns. - Example
.eslintrc:
{
"plugins": ["fict"],
"extends": ["plugin:fict/recommended"]
}- Recommended config mirrors compiler warnings so IDE diagnostics stay aligned with build output.
Is Fict production-ready? Alpha. Core is stable, but expect edge cases. Test thoroughly for critical apps.
Does Fict use a virtual DOM? No.
How does Fict handle arrays?
Default: immutable style (todos = [...todos, newTodo]). For deep mutations, you can use spread operation to create new immutable data, or use Immer/Mutative, or use $store from fict/plus.
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 ~8% of Solid in js-framework-benchmark.
Fict is built upon the brilliant ideas and relentless innovation of the open-source community. We would like to express our deepest respect and sincere gratitude to the following projects, whose work has been an indispensable source of inspiration and reference for Fict:
- React – For defining the modern era of UI development. Its component model and declarative philosophy set the standard for developer experience, a standard Fict strives to uphold.
- Solid – For pioneering fine-grained reactivity and demonstrating the power of compilation. Its architecture is the bedrock upon which Fict’s performance assertions are built.
- alien-signals – For pushing the boundaries of signal performance. Its advanced implementation details provided critical guidance for Fict’s reactive system.
We are profoundly grateful for their contributions to the web development world.
