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:

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()

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

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()

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

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 routes
  • pages/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

  1. Router builds a list of layouts mapped to directory paths
  2. When navigating, collects layouts for the destination path
  3. Calls each layout's protect function in order
  4. If any protect returns false, redirects to that layout's protectRedirect (or /)
  5. 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

↑