Components

Components in Effuse combine logic and UI in a clean, type-safe structure.

Basic Structure

Every component has a script and template:

import { define } from '@effuse/core';

const Greeting = define({
  script: () => {
    return { message: 'Hello, World!' };
  },
  template: ({ message }) => <h1>{message}</h1>,
});

Props

Components receive props from their parent. Use generics for type safety:

import { define, computed, unref, type Signal } from '@effuse/core';

interface ButtonProps {
  label: string;
  variant?: 'primary' | 'secondary';
  onClick?: () => void;
}

const Button = define<ButtonProps>({
  script: ({ props }) => ({
    label: props.label,
    variant: computed(() => unref(props.variant) ?? 'primary'),
    onClick: props.onClick,
  }),
  template: ({ label, variant, onClick }) => (
    <button class={`btn btn-${variant.value}`} onClick={onClick}>
      {label}
    </button>
  ),
});

// Usage
<Button label="Click me" variant="primary" onClick={handleClick} />;

Lifecycle Hooks

Components have access to lifecycle hooks via the script context:

import { define, signal } from '@effuse/core';

const Timer = define({
  script: ({ onMount }) => {
    const seconds = signal(0);

    onMount(() => {
      const interval = setInterval(() => {
        seconds.value++;
      }, 1000);

      // Return cleanup function
      return () => clearInterval(interval);
    });

    return { seconds };
  },
  template: ({ seconds }) => <p>Timer: {seconds} seconds</p>,
});

useCallback for Stable References

Use useCallback from the script context for event handlers that need stable references:

import { define, signal } from '@effuse/core';

const Form = define({
  script: ({ useCallback }) => {
    const inputValue = signal('');

    // Stable reference for event handler
    const handleInputChange = useCallback((e: Event) => {
      const input = e.currentTarget;
      if (!(input instanceof HTMLInputElement)) return;
      inputValue.value = input.value;
    });

    const handleSubmit = useCallback(() => {
      console.log('Submitted:', inputValue.value);
      inputValue.value = '';
    });

    return { inputValue, handleInputChange, handleSubmit };
  },
  template: ({ inputValue, handleInputChange, handleSubmit }) => (
    <div>
      <input value={inputValue} onInput={handleInputChange} />
      <button onClick={handleSubmit}>Submit</button>
    </div>
  ),
});

Children

Pass children to create wrapper components:

import { define } from '@effuse/core';

const Card = define({
  script: () => ({}),
  template: ({ children }) => <div class="card">{children}</div>,
});

// Usage
<Card>
  <h2>Title</h2>
  <p>Content goes here</p>
</Card>;

List Rendering with For

Use the For component for efficient list rendering:

import { define, signal, For } from '@effuse/core';

const TodoList = define({
  script: () => {
    const todos = signal([
      { id: 1, text: 'Learn Effuse' },
      { id: 2, text: 'Build an app' },
    ]);

    return { todos };
  },
  template: ({ todos }) => (
    <ul>
      <For each={todos} keyExtractor={(t) => t.id}>
        {(todoSignal) => <li>{todoSignal.value.text}</li>}
      </For>
    </ul>
  ),
});

For Props

PropTypeDescription
eachSignal<T[]>The signal containing the array to iterate
keyExtractor(item: T, index: number) => string or numberFunction to extract unique keys
fallbackJSX.ElementOptional element to render when array is empty

Conditional Rendering with Switch and Match

For multiple conditional branches, use the Switch and Match components:

import { define, signal, Switch, Match } from '@effuse/core';

const UserRole = define({
  script: () => {
    const role = signal<'admin' | 'user' | 'guest'>('guest');
    return { role };
  },
  template: ({ role }) => (
    <Switch fallback={<p>Unknown role</p>}>
      <Match when={() => role.value === 'admin'}>
        <p>Welcome, Administrator</p>
      </Match>
      <Match when={() => role.value === 'user'}>
        <p>Welcome, User</p>
      </Match>
      <Match when={() => role.value === 'guest'}>
        <p>Please sign in</p>
      </Match>
    </Switch>
  ),
});

Switch/Match Props

Switch Props

PropTypeDescription
fallbackJSX.ElementOptional element to render if no match is found

Match Props

PropTypeDescription
whenSignal<T> or () => TCondition to evaluate for truthiness

Conditional Rendering with Show

Use the Show component for conditional rendering based on signal values:

import { define, signal, Show } from '@effuse/core';

const UserProfile = define({
  script: () => {
    const user = signal<{ name: string } | null>(null);
    const login = () => {
      user.value = { name: 'John' };
    };
    const logout = () => {
      user.value = null;
    };

    return { user, login, logout };
  },
  template: ({ user, login, logout }) => (
    <div>
      <Show when={user} fallback={<button onClick={login}>Log in</button>}>
        {(u) => (
          <div>
            <p>Welcome, {u.name}!</p>
            <button onClick={logout}>Log out</button>
          </div>
        )}
      </Show>
    </div>
  ),
});

Show Props

PropTypeDescription
whenSignal<T> or () => TCondition to evaluate for truthiness
fallbackJSX.ElementElement to render when condition is falsy
children(value: T) => JSX.ElementRender function receiving the truthy value

Dynamic Component

The Dynamic component allows you to render different components dynamically based on a signal:

import { define, signal, Dynamic } from '@effuse/core';

const TabPanel = define({
  script: () => {
    const tabs = { home: HomeTab, settings: SettingsTab, profile: ProfileTab };
    const activeTab = signal<keyof typeof tabs>('home');

    const currentComponent = computed(() => tabs[activeTab.value]);

    return { activeTab, currentComponent };
  },
  template: ({ activeTab, currentComponent }) => (
    <div>
      <nav>
        <button
          onClick={() => {
            activeTab.value = 'home';
          }}
        >
          Home
        </button>
        <button
          onClick={() => {
            activeTab.value = 'settings';
          }}
        >
          Settings
        </button>
        <button
          onClick={() => {
            activeTab.value = 'profile';
          }}
        >
          Profile
        </button>
      </nav>
      <Dynamic component={currentComponent} fallback={<p>Loading...</p>} />
    </div>
  ),
});

Dynamic Props

PropTypeDescription
componentSignal<Component> or () => ComponentThe component to render dynamically
propsPProps to pass to the rendered component
fallbackJSX.ElementElement to render when component is null
portalsPortalsPortal configuration for the rendered component

Dynamic Styling

Use reactive functions for dynamic styles and classes:

import { define, signal, computed } from '@effuse/core';

const ColorBox = define({
  script: () => {
    const colors = ['mint', 'purple', 'cyan'];
    const index = signal(0);
    const currentColor = computed(() => colors[index.value]);
    const nextColor = () => {
      index.value = (index.value + 1) % colors.length;
    };

    return { currentColor, nextColor };
  },
  template: ({ currentColor, nextColor }) => (
    <div>
      <button onClick={nextColor}>Change Color</button>
      <div
        style={() => ({
          backgroundColor: `var(--accent-${currentColor.value})`,
          padding: '2rem',
          transition: 'background-color 0.3s ease',
        })}
      >
        Current: {currentColor.value}
      </div>
    </div>
  ),
});

Dynamic Classes

<div class={() => `card ${isActive.value ? 'active' : ''}`}>Content</div>

Repeat Component

The Repeat component renders content a specified number of times, with access to the current index:

import { define, signal, Repeat } from '@effuse/core';

const SkeletonLoader = define({
  script: () => {
    const count = signal(3);
    return { count };
  },
  template: ({ count }) => (
    <div class="skeleton-list">
      <Repeat times={count} fallback={<p>No items</p>}>
        {(index) => (
          <div class="skeleton-item">
            <div class="skeleton-avatar" />
            <div class="skeleton-content">
              <div class="skeleton-title" />
              <div class="skeleton-text" />
            </div>
            <span>Item {index + 1}</span>
          </div>
        )}
      </Repeat>
    </div>
  ),
});

Repeat Props

PropTypeDescription
timesnumber or Signal<number>Number of times to repeat the content
children(index: number) => ElementRender function receiving the current index
fallbackJSX.ElementOptional element to render when count is 0

Await Component

The Await component handles asynchronous data fetching with built-in pending, success, and error states:

import { define, signal, Await } from '@effuse/core';

interface User {
  id: number;
  name: string;
  email: string;
}

const UserProfile = define({
  script: () => {
    const fetchUser = (id: number): Promise<User> =>
      fetch(`https://api.example.com/users/${id}`).then((res) => res.json());

    const userPromise = signal(fetchUser(1));

    const refetch = () => {
      userPromise.value = fetchUser(Math.floor(Math.random() * 10) + 1);
    };

    return { userPromise, refetch };
  },
  template: ({ userPromise, refetch }) => (
    <div>
      <button onClick={refetch}>Fetch New User</button>
      <Await
        promise={userPromise}
        pending={
          <div class="loading">
            <span class="spinner" />
            Loading user data...
          </div>
        }
        error={(err) => (
          <div class="error">
            <p>Failed to load user</p>
            <p class="error-detail">{String(err)}</p>
          </div>
        )}
      >
        {(user) => (
          <div class="user-card">
            <h3>{user.name}</h3>
            <p>{user.email}</p>
            <span>ID: {user.id}</span>
          </div>
        )}
      </Await>
    </div>
  ),
});

Await Props

PropTypeDescription
promisePromise<T> or () => Promise<T> or Signal<Promise<T>>The promise to await
pendingJSX.Element or () => JSX.ElementElement to render while promise is pending
errorJSX.Element or (err: unknown) => JSX.ElementElement to render if promise rejects
children(data: T) => JSX.ElementRender function for successful promise resolution

Reactive Promise Changes

When using a Signal<Promise<T>>, the Await component automatically re-fetches when the signal's promise value changes:

const searchPromise = signal(fetchSearch('react'));

// Later, when you want to search for something else:
searchPromise.value = fetchSearch('effect');
// Await will automatically start loading the new promise

Suspense and ErrorBoundary

Handle loading and error states for nested components using Suspense and ErrorBoundary:

import { define, Suspense, ErrorBoundary } from '@effuse/core';
import { AsyncProfile } from './AsyncProfile';

const App = define({
  script: () => ({}),
  template: () => (
    <ErrorBoundary
      fallback={(err) => <div class="error">Fatal error: {String(err)}</div>}
    >
      <Suspense fallback={<div class="loading">Loading profile...</div>}>
        <AsyncProfile id={123} />
      </Suspense>
    </ErrorBoundary>
  ),
});

Suspense Props

PropTypeDescription
fallbackJSX.ElementElement to render while children are loading

ErrorBoundary Props

PropTypeDescription
fallbackEffuseChild &#124; ((err: Error, reset: () => void) => EffuseChild)Render function that receives the error
childrenEffuseChildThe content to monitor for errors
onError(err: Error) => voidOptional callback when an error is caught

Next Steps