Arfa.js Documentation
A modern TSX-based framework for building fast, component-driven web applications
π Overview
Arfa.js is a lightweight framework that compiles TSX into optimized JavaScript, powered by Vite and the custom Arfa runtime engine.
It offers a React-like component model and a Next.js-style file-based router, making it both familiar and easy to adopt.
Created by Arman Tarhani, Arfa.js aims to provide simplicity, speed, and flexibility out of the box.
β¨ Features
β‘ Blazing Fast
Vite-powered dev server and builds with near-instant hot module replacement.
π¨ TailwindCSS Ready
Use Tailwind by default with zero setup required. Just start writing classes.
π§© TSX/JSX Support
Write strongly-typed UI components with full TypeScript support.
βοΈ Custom Runtime
Lightweight, optimized rendering engine designed for performance.
π§΅ Reactive Hooks
Built with arfa-reactives for state and lifecycle management.
π« Zero Config
Sensible defaults with easy overrides when you need customization.
π Getting Started
Quick Start
Create a new Arfa.js project with:
npx create-arfa my-app
cd my-app
npm install
npm run dev
This will:
- Scaffold a new project with the default template
- Install all dependencies
- Start the development server at
http://localhost:3000
Note: You'll need Node.js 16.x or later installed on your system.
π Reactivity System
Arfa.js uses the arfa-reactives package to provide a familiar but lightweight hook system.
Core Hooks
ref(initialValue)β Create reactive state (returns[getter, setter])onMounted(fn)β Run logic when a component is first mountedonEffect(fn, deps)β Run side effects when dependencies change
ref()
ref creates a reactive value local to the current component instance.
Signature
const [value, setValue] = ref<T>(initial: T)
Reading and writing
import { ref } from "arfa-reactives";
export default function Counter() {
const [count, setCount] = ref(0);
// Read current value
const n = count();
// Set next value (direct)
setCount(n + 1);
// Functional update
setCount(prev => prev + 1);
return (
<button onClick={() => setCount(c => c + 1)}>Clicked {count()}</button>
);
}
Tip: Always call the getter (e.g. count()) to read the current value.
onMounted()
Runs once after the component's initial render. Use it for subscriptions, event listeners, or DOM access.
Signature
onMounted(effect: () => void): void
Example
import { onMounted, ref } from "arfa-reactives";
export default function Clock() {
const [now, setNow] = ref(new Date().toLocaleTimeString());
onMounted(() => {
const id = setInterval(() => setNow(new Date().toLocaleTimeString()), 1000);
// Clean up using onEffect with [] or return cleanup in onEffect; see below.
// For simplicity, use onEffect(() => () => clearInterval(id), [])
});
return <div>{now()}</div>;
}
onEffect()
Runs after render when any dependency changes. Return a function to clean up the previous effect.
Signature
onEffect(effect: () => (void | (() => void)), deps: Array<() => any>): void
Examples
import { onEffect, ref } from "arfa-reactives";
export default function FetchUser({ id }: { id: string }) {
const [user, setUser] = ref<null | { name: string }>(null);
// Re-run when id changes
onEffect(() => {
let cancelled = false;
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(data => { if (!cancelled) setUser(data); });
// Cleanup runs before next effect and on unmount:
return () => { cancelled = true; };
}, [() => id]);
return user() ? <h3>Hello {user()!.name}</h3> : <p>Loading...</p>;
}
// Multiple dependencies
onEffect(() => {
console.log("Deps changed:", a(), b());
}, [a, b]);
Patterns & Tips
- Always pass getters in the deps array:
[count]not[count()]. - Cleanup: return a function from
onEffectto dispose timers, listeners, etc. - Functional updates avoid stale reads:
setCount(c => c + 1). - Read values inside render/effect via
refGetter()to get the latest. - Avoid side-effects during render; use
onMounted/onEffectinstead.
Full Example
import { onMounted, onEffect, ref } from "arfa-reactives";
export default function CounterExample() {
const [count, setCount] = ref(1);
const [showMessage, setShowMessage] = ref(true);
// Run once on mount
onMounted(() => {
console.log("Component mounted with initial count:", count());
});
// Effect runs when count changes
onEffect(() => {
console.log("Count changed:", count());
return () => console.log("Cleaning up for count:", count());
}, [count]);
// Effect runs when showMessage changes
onEffect(() => {
console.log("Show message changed:", showMessage());
}, [showMessage]);
return (
<div>
<h2>Current count: {count()}</h2>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={() => setShowMessage(v => !v)}>Toggle Message</button>
{showMessage() && (
<p>{count() % 2 === 0 ? "Count is even!" : "Count is odd!"}</p>
)}
</div>
);
}
Remember: Access reactive values via their getter (e.g., count()) inside
effects and render.
π§ Context API
The Context API lets you pass values through the component tree without prop drilling. It integrates with
Arfaβs reactive ref so consumers re-render automatically when the provided value changes.
Overview
createContext<T>(defaultValue: T)β create a context object.withContext(ctx, valueOrGetter, render)β provide a value (or a ref getter) for descendants.useContext(ctx)β read the nearest provided value (auto-subscribes if you provided a getter).
createContext()
import { createContext } from "arfa-reactives";
const CountCtx = createContext<number>(0);
withContext()
Provide a plain value or a ref getter. Passing the getter makes consumers reactive.
import { withContext, ref } from "arfa-reactives";
const [countRef, setCount] = ref(0);
return withContext(CountCtx, countRef, () => {
// children here can call useContext(CountCtx)
return <Child />;
});
useContext()
import { useContext } from "arfa-reactives";
function Child() {
const count = useContext(CountCtx); // number
return <div>Count: {count}</div>;
}
Persisted Counter (Full Example)
This example persists the counter in localStorage and provides it via Context so any child
can read it.
import {
ref,
createContext,
useContext,
withContext,
onMounted,
onEffect
} from "arfa-reactives";
// Full storage key = keyPrefix + key
const COUNT_KEY = "arfa:docs:count";
const CountCtx = createContext<number>(0);
// Optional: seed from localStorage for first paint (SSR-safe)
function readInitialCount(defaultValue = 0): number {
try {
if (typeof window === "undefined") return defaultValue;
const raw = window.localStorage.getItem(COUNT_KEY);
if (!raw) return defaultValue;
const env = JSON.parse(raw) as { v?: number; d: unknown };
const n = Number((env as any).d);
return Number.isFinite(n) ? n : defaultValue;
} catch {
return defaultValue;
}
}
export default function ContextCounterPage() {
// Persisted store
const [countRef, setCount] = ref<number>(readInitialCount(0), {
persist: {
key: "docs:count", // stored under "arfa:docs:count"
version: 1,
keyPrefix: "arfa:",
// sync: true // default: cross-tab updates
},
});
onMounted(() => {
console.log("Mounted. Hydrated count =", countRef());
});
onEffect(() => {
console.log("Count changed ->", countRef());
}, [countRef]);
const inc = () => setCount(c => (c ?? 0) + 1);
const dec = () => setCount(c => (c ?? 0) - 1);
const reset = () => setCount(0);
// Provide the getter so consumers auto-update
return withContext(CountCtx, countRef, () => {
const count = useContext(CountCtx);
return (
<div class="p-3 border rounded">
<h3>Counter via Context (Persisted)</h3>
<p>Current: {count}</p>
<div class="flex gap-2">
<button class="btn" onClick={inc}>+1</button>
<button class="btn" onClick={dec}>-1</button>
<button class="btn" onClick={reset}>Reset</button>
</div>
<p class="code" style="margin-top:12px">
Persisted under localStorage key: <code>arfa:docs:count</code>
</p>
</div>
);
});
}
Tips: Pass the getter (e.g. countRef) to
withContext so consumers re-render on updates. In dependency arrays use getters (e.g.
[countRef]), not values (no countRef()).
π File-based Routing
Arfa.js uses a file-system based router where routes are defined by files in the pages
directory, similar to Next.js.
Basic Routes
pages/index.tsxβ/pages/about.tsxβ/aboutpages/contact.tsxβ/contact
Each .tsx file in the pages directory becomes a route automatically.
Nested Routes
- pages/blog/
index.tsxβ/blog[slug].tsxβ/blog/:sluglatest.tsxβ/blog/latest
Dynamic Routes
- pages/users/
[id].tsxβ/users/:id
- pages/posts/
- [category]/
[id].tsxβ/posts/:category/:id
- [category]/
Dynamic segments are accessed via props.params in your components.
Layout System
Create layouts by adding _layout.tsx files:
pages/_layout.tsxβ Applies to all routespages/about.tsx- pages/blog/
_layout.tsxβ Applies to /blog/*index.tsx[slug].tsx
Layouts can be nested and will compose automatically.
π Protected Routes
Arfa.js provides a simple way to protect routes using layout files.
How It Works
- Router builds a list of layouts mapped to directory paths
- When navigating, collects layouts for the destination path
- Calls each layout's
protectfunction in order - If any
protectreturns false, redirects to that layout'sprotectRedirect(or /) - If all guards pass, renders the page wrapped in layouts
Basic Example
import { FC, GuardFn } from "arfa-types";
/**
* Authentication guard for protected routes.
* Returns `true` if access is granted, `false` otherwise.
* Put your real auth logic here (token/session check).
*/
export const protect: GuardFn = () => {
// TODO: Implement actual authentication logic
return true;
};
/**
* Redirect path for unauthorized access.
* Router will send users here if `protect` returns false.
*/
export const protectRedirect = "/login" as const;
// If your router passes params to layouts, include them:
const DashboardLayout: FC = ({ children }) => {
return (
<div class="admin-shell">
<aside>Admin menu</aside>
<main>{children}</main>
</div>
);
};
export default DashboardLayout;
The protect function can be synchronous or asynchronous.
Async Guard Example
// pages/dashboard/_layout.tsx
export async function protect() {
const token = localStorage.getItem("token");
if (!token) return false;
const ok = await fetch("/api/validate", {
headers: { Authorization: `Bearer ${token}` }
})
.then(r => r.ok)
.catch(() => false);
return ok;
}
export const protectRedirect = "/login";
export default function DashboardLayout({ children }: any) {
return <div class="dashboard">{children}</div>;
}
π¦ Project Structure
A typical Arfa.js project looks like this:
source/
βββ pages/ # Routes and layouts
β βββ _layout.tsx # Root layout
β βββ index.tsx # Home page
β βββ 404.tsx # Not found page
β βββ dashboard/
β βββ _layout.tsx # Admin layout
β βββ index.tsx
βββ assets/ # Static files
βββ layout/ # optional
βββ core.tsx # core script
βββ styles.css # global styles
βββ package.json
You can customize this structure in arfa.config.ts if needed.
π Contact
For any inquiries, please contact: armantarhani1997@gmail.com