Hooks

Hooks in Effuse provide reusable, composable logic with built-in lifecycle management. Create custom hooks using defineHook from @effuse/core.

For production browser and timing utilities, install @effuse/use and see Utility Hooks. Keep defineHook for application-specific composition and capability ownership.

Creating a Hook

Use defineHook to create typed, reusable hooks:

import { defineHook, type Signal } from '@effuse/core';

interface ToggleConfig {
  initial?: boolean;
}

interface ToggleReturn {
  isOpen: Signal<boolean>;
  toggle: () => void;
  open: () => void;
  close: () => void;
}

export const useToggle = defineHook<ToggleConfig, ToggleReturn>({
  name: 'useToggle',
  setup: ({ config, signal }): ToggleReturn => {
    const isOpen = signal(config.initial ?? false);

    return {
      isOpen,
      toggle: () => {
        isOpen.value = !isOpen.value;
      },
      open: () => {
        isOpen.value = true;
      },
      close: () => {
        isOpen.value = false;
      },
    };
  },
});

Hook Context

The setup function receives a context object with these utilities:

PropertyDescription
configConfiguration passed when calling the hook
signalCreate reactive signals
computedCreate derived computed values
watchEffectRun side effects that track dependencies
onMountRegister callbacks for when the hook is mounted
layersTyped access to declared layers' props and services
scopeManage cleanup and finalizers

Built-in Utility Hooks

Effuse provides several built-in hooks within the script context for common patterns:

useCallback(fn, deps?)

Memoizes a function to maintain a stable identity across renders.

const handleClick = useCallback(() => {
  console.log('Clicked!', count.value);
}, [count]);

useMemo(fn, deps?)

Memoizes a computed value. Useful for expensive calculations that don't need to be reactive signals themselves but should be cached.

const expensiveValue = useMemo(() => {
  return performHeavyCalculation(props.data);
}, [props.data]);

useStore(name)

Access a global store by name.

const todos = useStore('todos');

Using Hooks in Components

Call hooks in your component's script function:

import { define } from '@effuse/core';
import { useToggle } from '../hooks';

const Dropdown = define({
  script: ({ onMount }) => {
    const menu = useToggle({ initial: false });

    return {
      isOpen: menu.isOpen,
      toggle: menu.toggle,
    };
  },
  template: ({ isOpen, toggle }) => (
    <div>
      <button onClick={toggle}>{isOpen.value ? 'Close' : 'Open'}</button>
      {isOpen.value && <div class="menu">Menu Content</div>}
    </div>
  ),
});

DOM-Dependent Hooks

For hooks that need DOM access, use a lazy initialization pattern:

import { defineHook, type Signal } from '@effuse/core';

interface ClickOutsideConfig {
  selector: string;
}

interface ClickOutsideReturn {
  onClickOutside: (callback: () => void) => void;
  init: () => void;
}

export const useClickOutside = defineHook<
  ClickOutsideConfig,
  ClickOutsideReturn
>({
  name: 'useClickOutside',
  setup: ({ config, signal, watchEffect }): ClickOutsideReturn => {
    const initialized = signal(false);
    let callback: (() => void) | null = null;

    watchEffect(() => {
      if (!initialized.value) return undefined;

      const handleClick = (e: Event) => {
        const target = e.target;
        if (!(target instanceof Element)) return;
        if (!target.closest(config.selector)) {
          callback?.();
        }
      };

      document.addEventListener('click', handleClick);
      return () => document.removeEventListener('click', handleClick);
    });

    return {
      onClickOutside: (cb) => {
        callback = cb;
      },
      init: () => {
        initialized.value = true;
      },
    };
  },
});

Use in a component:

const Dropdown = define({
  script: ({ onMount }) => {
    const toggle = useToggle({ initial: false });
    const clickOutside = useClickOutside({ selector: '.dropdown' });

    onMount(() => {
      clickOutside.onClickOutside(() => toggle.close());
      clickOutside.init();
      return undefined;
    });

    return { isOpen: toggle.isOpen, toggle: toggle.toggle };
  },
  template: ({ isOpen, toggle }) => (
    <div class="dropdown">
      <button onClick={toggle}>Menu</button>
      {isOpen.value && <div class="menu">Content</div>}
    </div>
  ),
});

Accessing Layers from Hooks

Hooks can access layer state and services:

import { defineHook } from '@effuse/core';
import { I18nLayer } from './layers/I18nLayer';

const layers = { i18n: I18nLayer } as const;

export const useTranslation = defineHook<
  undefined,
  { t: (key: string) => string },
  typeof layers
>({
  name: 'useTranslation',
  layers,
  setup: ({ layers: { i18n } }) => {
    const translations = i18n.prop('translations');

    return {
      t: (key: string) => translations.value?.[key] ?? key,
    };
  },
});

Cleanup

Watch effects automatically clean up when the component unmounts. Return a cleanup function from watchEffect:

watchEffect(() => {
  const handler = () => {
    /* ... */
  };
  window.addEventListener('resize', handler);

  // Cleanup runs when the watcher re-runs or the component unmounts
  return () => window.removeEventListener('resize', handler);
});

Next Steps