
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
onEffect
to 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
/onEffect
instead.
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
β/about
pages/contact.tsx
β/contact
Each .tsx file in the pages directory becomes a route automatically.
Nested Routes
- pages/blog/
index.tsx
β/blog
[slug].tsx
β/blog/:slug
latest.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
protect
function in order - If any
protect
returns 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