A modern TSX-based framework for building fast, component-driven web applications.
npm install create-arfaA modern TSX-based framework for building fast, component-driven web applications.
see the documentation of Arfa JS
---
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.
---
- โก Blazing Fast โ Vite-powered dev server and builds
- ๐จ TailwindCSS Ready โ Use Tailwind by default with zero setup
- ๐งฉ TSX/JSX Support โ Write strongly-typed UI components
- โ๏ธ Custom Runtime โ Lightweight, optimized rendering engine
- ๐งต Reactive Hooks โ Built with arfa-reactives for state and lifecycle
- ๐ซ Zero Config โ Sensible defaults with easy overrides
- ๐ TypeScript Native โ First-class TypeScript support
---
``bash`
npx create-arfa my-app
cd my-app
npm install
npm run dev
For any inquiries, please contact: armantarhani1997@gmail.com
Arfa.js uses the arfa-reactives package to provide a familiar but lightweight hook system:
ref(initialValue) โ Create reactive state ([getter, setter])
onMounted(fn) โ Run logic when a component is mounted
onEffect(fn, deps) โ Run side effects when dependencies change
Example Usage:
`bash
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 (
{showMessage() && (
{count() % 2 === 0 ? "Count is even!" : "Count is odd!"}
`
Arfa.js uses a file-system based router where routes are defined by files in the pages directory. very similar with next js!
`bash`
pages/
index.tsx โ /
about.tsx โ /about
contact.tsx โ /contact
Nested Routes:
`bash`
pages/
blog/
index.tsx โ /blog
[slug].tsx โ /blog/:slug
latest.tsx โ /blog/latest
Dynamic Routes:
`bash`
pages/
users/
[id].tsx โ /users/:id
posts/
[category]/
[id].tsx โ /posts/:category/:id
Layout System: Create layouts by adding \_layout.tsx files:
`bash`
pages/
_layout.tsx โ Applies to all routes
about.tsx
blog/
_layout.tsx โ Applies to /blog/*
index.tsx
[slug].tsx
How it works (brief)
Router builds a list of layouts (from files named \_layout.tsx) mapped to directory paths (e.g. core/pages/admin/\_layout.tsx โ "/admin").
When navigating, the router collects directory list for the destination (e.g. /admin/settings โ ["/", "/admin", "/admin/settings"]) and calls each layout's protect in that order.
If any protect returns false (or resolves to false) the router redirects to that layout's protectRedirect (or /).
If all guards pass, the page is rendered wrapped in layouts and optional \_app.
`bash
export function protect(params?: any, pathname?: string): boolean | Promise
// or as a property exported from default (both are supported by router)
export default function MyLayout(...) { ... }
export const protect = () => true;
export const protectRedirect = "/login";
`
Example:
`bash
export function protect() {
return !!localStorage.getItem("token");
}
export const protectRedirect = "/login";
export default function AdminLayout({ children }: any) {
return (
`
Async guard (e.g. validate token by calling an API):
`bashBearer ${token}
//core/pages/dashboard/_layout.tsx
export async function protect() {
const token = localStorage.getItem("token");
if (!token) return false;
// fake async check
const ok = await fetch("/api/validate", { headers: { Authorization: } })
.then(r => r.ok)
.catch(() => false);
return ok;
}
export const protectRedirect = "/login";
export default function DashboardLayout({ children }: any) {
return
`
Arfa.js provides a Context API that allows you to share state across components without the need to pass props manually. It is reactive, and consumers re-render automatically when the provided value changes.
The createContext function is used to create a new context with a default value.
`bash
import { createContext } from "arfa-reactives";
// Create context with a default value
const CountCtx = createContext
`
The withContext function allows you to provide a value to the context. This value can be a plain value or a reactive reference getter (ref).
`bash
import { createContext } from "arfa-reactives";
// Create context with a default value
const CountCtx = createContext
import { withContext, ref } from "arfa-reactives";
const [countRef, setCount] = ref(0);
return withContext(CountCtx, countRef, () => {
// Children here can call useContext(CountCtx)
return
});
`
The useContext hook allows you to consume the nearest context value, whether it's a static value or a reactive reference.
`bash
import { useContext } from "arfa-reactives";
function Child() {
const count = useContext(CountCtx); // number
return
`
This example demonstrates how to use context with persistence (e.g., in localStorage). The counter is persisted across page refreshes.
`bash
import { ref, createContext, useContext, withContext, onMounted, onEffect } from "arfa-reactives";
// Full storage key = keyPrefix + key
const COUNT_KEY = "arfa:docs:count";
const CountCtx = createContext
// 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
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 (
Current: {count}
Persisted under localStorage key: arfa:docs:count
``