Hooks

PawaJS provides a set of powerful hooks to manage component state, lifecycle, and side effects.

$state

The core of PawaJS reactivity. Creates a reactive object that automatically triggers UI updates when its .value changes. Can be used inside or outside components.

$state(initialValue, section?)

  • initialValue — any value, or a function that returns a value / Promise
  • sectionstring for localStorage key, or Array for computed dependencies
  • $state can be export and used globally as module export.

Basic usage

js

                 
import { $state } from 'pawajs';

// Simple values
export const count = $state(0);
const name = $state('Pawa');
const user = $state({ name: 'Alice', age: 30 });
const items = $state(['apple', 'banana']);

// Reading & writing
console.log(count.value);   // 0
count.value++;              // triggers updates
user.value.name = 'Bob';    // deep reactivity 

        

Persistent state (localStorage)

Pass a string as the second argument. The state is automatically hydrated from localStorage and debounced-saved on every change.

js

                 
// Persisted under the key "theme"
const theme = $state('light', 'theme');

// Persisted object
const settings = $state({ notifications: true, density: 'comfortable' }, 'user-settings'); 

        

Computed state

Pass an array of dependencies as the second argument and a function as the first. Must be used inside a component.

js

                 
const count = $state(0);
const double = $state(() => count.value * 2, [count]);

// double.value automatically updates when count changes 

        

Async state

When initialValue is a function that returns a Promise, the state becomes async-aware.

js

                 
const user = $state(async () => {
    const res = await fetch('/api/user');
    return res.json();
});

// While loading:
user.async === true

// After resolve:
user.async === false
user.value   // the resolved data

// On failure:
user.failed === true
user.retry() // re-runs the original function 

        

Return shape: { value, id, async?, failed?, retry? }. The id is internal — do not modify it.

runEffect

Runs side effects with flexible dependency control. The callback may return a cleanup function that is called on unmount or when dependencies change.

runEffect(callback, deps)

callback: () => (() => any) | void

  • null / undefined → Mount effect (runs once after mount)
  • number → Before-mount effect (delayed by N milliseconds if global module use case)
  • Array → Watch specific states (re-runs when any dependency changes)
  • object → Readonly reactive effect (tracks any state read inside the callback)

Mount effect (null)

js

                 
runEffect(() => {
    console.log('Component mounted');

    // Optional cleanup
    return () => {
        console.log('Component unmounted');
    };
}, null); 

        

Before-mount (delayed)

Pass any number to run the effect after the given milliseconds (global).But the number doesn't do anything in the component just before the component is considered fully mounted.

js

                 
runEffect(() => {
    console.log('Running 100ms before mount logic finishes');
}, 100); 

        

Watch specific dependencies (Array)

js

                 
const count = $state(0);
const name = $state('Pawa');

runEffect(() => {
    console.log('count or name changed:', count.value, name.value);

    return () => {
        // cleanup from previous run
    };
}, [count, name]); 

        

Readonly reactive effect (object)

Pass any object (commonly {}). The effect automatically tracks every reactive state that is read inside the callback. Ideal when you don’t want to list dependencies manually.

js

                 
const width = $state(window.innerWidth);
const height = $state(window.innerHeight);

runEffect(() => {
    // Automatically re-runs when width or height changes
    console.log(`Viewport: ${width.value} × ${height.value}`);
}, {}); 

        

Outside components

runEffect also works at module level:

  • null → runs via microtask
  • number → uses setTimeout
  • Array → uses internal stateWatch
  • object → creates a live effect immediately

Cleanups registered outside components are automatically attached to window.beforeunload.

useInsert

Makes variables, state, and functions available within a component's template. It's how you "inject" your component's logic into its HTML.

js

                 
import { $state, useInsert, html } from 'pawajs';

const MyComponent = () => {
    const name = $state('World');
    const greet = () => `Hello, ${name.value}!`;

    useInsert({ name, greet }); // Expose 'name' state and 'greet' function

    return html`
        <h1>@{greet()}</h1>
        <input type="text" value="@{name.value}" on-input="name.value = e.target.value">
    `;
}; 

        

useContext & setContext

The Context API allows you to pass data deep down the component tree without manually passing props at every level.

js

                 
import { setContext, useContext, useInsert, html, $state } from 'pawajs';

const ThemeContext = setContext();

const ThemeProvider = ({ children }) => {
    const theme = $state('light');
    ThemeContext.setValue({ theme });
    useInsert({ children });
    return html`<div>${children}</div>`;
};

const ThemeButton = () => {
    const { theme } = useContext(ThemeContext);
    useInsert({ theme });
    return html`
        <button on-click="theme.value = theme.value === 'light' ? 'dark' : 'light'">
            Toggle Theme: @{theme.value}
        </button>
    `;
}; 

        

useRef

Creates a mutable ref object that can hold a reference to a DOM element. Use it with the ref directive.

js

                 
import { useRef, useInsert, html, runEffect } from 'pawajs';

const FocusInput = () => {
    const inputRef = useRef();

    runEffect(() => {
        if (inputRef.value) {
            inputRef.value.focus();
        }
    }, null);

    useInsert({ inputRef });

    return html`
        <input ref="inputRef" type="text" placeholder="I will be focused!">
    `;
}; 

        

useAsync

Enables asynchronous component rendering. Use $async to wrap PawaJS hooks that depend on awaited values.

js

                 
import { useAsync, $state, useInsert, html } from 'pawajs';

const AsyncUserComponent = async () => {
    const { $async, onSuspense } = useAsync();

    onSuspense(html`<div>Loading user data...</div>`);

    const response = await fetch('/api/user/1');
    const userData = await response.json();

    const user = $async(() => $state(userData));
    $async(() => useInsert({ user }));

    return html`
        <div>
            <h2>@{user.value.name}</h2>
            <p>Email: @{user.value.email}</p>
        </div>
    `;
}; 

        

forwardProps

Explicitly forwards unconsumed props (including rest props --) to a nested element or component.

js

                 
import { forwardProps, useInsert, html } from 'pawajs';

const MyWrapperButton = (props) => {
    forwardProps(props);
    useInsert({ props });

    return html`
        <div class="wrapper">
            <button -->Click Me</button>
        </div>
    `;
}; 

        
// ... (keep the existing header, $state, runEffect, useInsert sections) ...

useValidateComponent

Attaches runtime prop validation rules to a component. The rules are stored on the component function itself and enforced when the component is rendered.

useValidateComponent(component, rules)

  • component — the component function (must be a named function)
  • rules — object describing expected props
js

                 
import { useValidateComponent, html } from 'pawajs';

const UserCard = ({ name, age, isAdmin }) => {
    return html`
        <div>
            <h2>@{name()}</h2>
            <p>Age: @{age()}</p>
            <span if="isAdmin()">Admin</span>
        </div>
    `;
};

useValidateComponent(UserCard, {
    name: {
        type: String,
        strict: true,          // required
        err: 'Name is required'
    },
    age: {
        type: Number,
        default: 0
    },
    isAdmin: {
        type: Boolean,
        default: false
    }
}); 

        

The component must be a named function.

setContext & useContext

Lightweight context system for passing data down the component tree without prop drilling.

setContext()
Creates a context handle
Returns { id, setValue }. Call setValue(data) inside a provider component.
useContext(context)
Consumes a context
Pass the handle returned by setContext(). Must be called inside a component.
js

                 
import { setContext, useContext, $state, useInsert, html, RegisterComponent } from 'pawajs';

// 1. Create the context handle (usually at module level)
const ThemeContext = setContext();

// 2. Provider
const ThemeProvider = ({ children }) => {
    const theme = $state('light');

    // Make the value available to descendants
    ThemeContext.setValue({ theme });

    useInsert({ children });

    return html`<div class="theme-@{theme.value}">${children}</div>`;
};

// 3. Consumer
const ThemeToggle = () => {
    const { theme } = useContext(ThemeContext);

    useInsert({ theme });

    return html`
        <button on-click="theme.value = theme.value === 'light' ? 'dark' : 'light'">
            Switch to @{theme.value === 'light' ? 'dark' : 'light'} mode
        </button>
    `;
};

RegisterComponent(ThemeProvider, ThemeToggle); 

        

Important: setValue must be called during the component’s setup phase (before it has fully run). Calling it after the component has rendered is ignored.

useInnerContext

Returns the context of the immediate parent element/component (the element context created by inline state-* attributes or the parent component). Useful for tightly coupled child components.

js

                 
import { useInnerContext, useInsert, html, $state, RegisterComponent } from 'pawajs';

const Parent = () => {
    const title = $state('Dashboard');
    useInsert({ title });

    return html`
        <div state-title="title.value">
            <h1>@{title.value}</h1>
            <child-panel></child-panel>
        </div>
    `;
};

const ChildPanel = () => {
    // Access the parent's element context
    const parentCtx = useInnerContext();

    useInsert({ parentCtx });

    return html`
        <p>Title from parent: @{parentCtx.title.value}</p>
    `;
};

RegisterComponent(Parent, ChildPanel); 

        

useServer

Bridge between server-side rendering and client-side continuity. Allows a component to serialize data on the server and retrieve it during hydration.

const { setServerData, getServerData } = useServer()

  • setServerData(data) — store data while rendering on the server
  • getServerData() — retrieve the serialized data during client resume
js

                 
import { useServer, $state, useInsert, html, isResume } from 'pawajs';

const UserProfile = () => {
    const { setServerData, getServerData } = useServer();

    let initial = { name: 'Loading…', email: '' };

    if (isResume()) {
        // Client-side continuity – read what the server sent
        initial = getServerData() || initial;
    } else {
        // Server-side only
        // setServerData({ name: 'Jane Doe', email: 'jane@example.com' });
    }

    const user = $state(initial);
    useInsert({ user });

    return html`
        <div>
            <h2>@{user.value.name}</h2>
            <p>@{user.value.email}</p>
        </div>
    `;
}; 

        

On the client, setServerData is a no-op and logs a warning. It only has an effect during server rendering.

accessChild

Server-only helper. Returns undefined on the client. Used internally by the SSR engine to access child content during server rendering. Most applications do not need to call this directly.