All files / react-classic/src lifecycle.ts

100% Statements 107/107
100% Branches 24/24
100% Functions 9/9
100% Lines 107/107

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 1791x     1x 1x 1x                                     1x 5x 5x   5x 5x 5x 5x   5x 5x 5x 5x 1x 1x   1x 1x 1x 1x 5x 5x 5x 5x 5x 4x 2x 4x 4x 2x 4x   4x 4x 4x 5x 5x 5x 5x 5x 5x   5x 5x 5x   5x 5x 5x 5x 5x 5x 5x 5x 5x 5x                                 1x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x   3x   3x 2x 2x 3x   3x 5x 5x 3x 2x 2x   3x 3x 3x   3x 3x                       1x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x   2x 2x                       1x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x   2x 2x  
import { captureStack, createObserver, microtask, untrack } from '@anchorlib/core';
import type { CleanupHandler, Lifecycle, MountHandler, SideEffectCleanup, SideEffectHandler } from './types.js';
 
let currentMountHandlers: Set<MountHandler> | null = null;
let currentMountCleanups: Set<CleanupHandler> | null = null;
let currentEffectCleanups: Set<SideEffectCleanup> | null = null;
 
/**
 * Creates a new lifecycle manager for handling component mount, cleanup, and rendering operations.
 *
 * The lifecycle manager provides three core methods:
 * - `mount()`: Schedules and executes mount handlers and effects
 * - `cleanup()`: Schedules and executes cleanup handlers and clears all handlers
 * - `render()`: Executes a render function within the component's context
 *
 * This function manages:
 * - Mount handlers registered via `onMount()`
 * - Cleanup handlers registered via `onCleanup()`
 * - Effects registered via `effect()`
 *
 * It also handles the proper execution order and cleanup of effects with their cleanup functions.
 *
 * @returns A Lifecycle object with mount, cleanup, and render methods
 */
export function createLifecycle(): Lifecycle {
  const [scheduleMount, cancelMount] = microtask(0);
  const [scheduleCleanup, cancelCleanup] = microtask(0);
 
  const mountHandlers = new Set<MountHandler>();
  const mountCleanups = new Set<CleanupHandler>();
  const effectHandlers = new Set<SideEffectHandler>();
  const effectCleanups = new Set<SideEffectCleanup>();
 
  return {
    mount() {
      cancelCleanup();
      scheduleMount(() => {
        mountHandlers.forEach((mount) => {
          const cleanup = mount();
 
          if (typeof cleanup === 'function') {
            mountCleanups.add(cleanup);
          }
        });
      });
    },
    cleanup() {
      cancelMount();
      scheduleCleanup(() => {
        mountCleanups.forEach((cleanup) => {
          cleanup();
        });
        effectCleanups.forEach((effectCleanup) => {
          effectCleanup();
        });
 
        mountHandlers.clear();
        mountCleanups.clear();
        effectHandlers.clear();
      });
    },
    render<R>(fn: () => R) {
      const prevMountHandlers = currentMountHandlers,
        prevCleanupHandlers = currentMountCleanups,
        prevEffectCleanups = currentEffectCleanups;
 
      currentMountHandlers = mountHandlers;
      currentMountCleanups = mountCleanups;
      currentEffectCleanups = effectCleanups;
 
      try {
        return untrack(fn) as R;
      } finally {
        currentMountHandlers = prevMountHandlers;
        currentMountCleanups = prevCleanupHandlers;
        currentEffectCleanups = prevEffectCleanups;
      }
    },
  };
}
 
/**
 * Registers an effect handler function that will be executed during the component's lifecycle.
 *
 * Effects are used for side effects that need to be cleaned up, such as subscriptions or timers.
 * They are executed after the component is mounted and will re-run when any state accessed within
 * the effect handler changes. Effects can optionally return a cleanup function which will be
 * executed before the effect runs again or when the component is unmounted.
 *
 * Note: Effects should typically not mutate state that they also observe, as this can lead to
 * circular updates and infinite loops.
 *
 * @param fn - The effect handler function to register
 *
 * @throws {Error} If called outside a Setup component context
 */
export function effect(fn: SideEffectHandler) {
  if (!currentEffectCleanups) {
    const error = new Error('Out of Setup component.');
    captureStack.violation.general(
      'Effect handler declaration violation detected:',
      'Attempted to use effect handler outside of Setup component.',
      error,
      undefined,
      effect
    );
  }
 
  let cleanup: SideEffectCleanup | void;
 
  const observer = createObserver((event) => {
    cleanup?.();
    runEffect(event);
  });
 
  const runEffect: SideEffectHandler = (event) => {
    cleanup = observer.run(() => fn(event));
  };
  const leaveEffect = () => {
    cleanup?.();
  };
 
  if (typeof window !== 'undefined') {
    runEffect({ type: 'init', keys: [] });
  }
 
  currentEffectCleanups?.add(leaveEffect);
}
 
/**
 * Registers a mount handler function that will be executed when the component is mounted.
 *
 * Mount handlers are executed when the component is being set up and can optionally
 * return a cleanup function that will be called when the component is unmounted.
 *
 * @param fn - The mount handler function to register
 *
 * @throws {Error} If called outside a Setup component context
 */
export function onMount(fn: MountHandler) {
  if (!currentMountHandlers) {
    const error = new Error('Out of Setup component.');
    captureStack.violation.general(
      'Mount handler declaration violation detected:',
      'Attempted to use mount handler outside of Setup component.',
      error,
      undefined,
      onMount
    );
  }
 
  currentMountHandlers?.add(fn);
}
 
/**
 * Registers a cleanup handler function that will be executed when the component is cleaned up.
 *
 * Cleanup handlers are executed when the component is being torn down, typically to
 * clean up resources like event listeners, timers, or subscriptions.
 *
 * @param fn - The cleanup handler function to register
 *
 * @throws {Error} If called outside a Setup component context
 */
export function onCleanup(fn: CleanupHandler) {
  if (!currentMountCleanups) {
    const error = new Error('Out of Setup component.');
    captureStack.violation.general(
      'Cleanup handler declaration violation detected:',
      'Attempted to use cleanup handler outside of Setup component.',
      error,
      undefined,
      onCleanup
    );
  }
 
  currentMountCleanups?.add(fn);
}