Skip to content
Download

Component lifecycle

The lifecycle system provides a declarative way to manage component lifecycles during page transitions, ensuring smooth animations and proper cleanup. Components are automatically discovered and managed based on data attributes.

The system provides a declarative way to manage component lifecycles during page transitions, ensuring smooth animations and proper cleanup. Components are automatically discovered and managed based on data attributes.

Components are automatically discovered using the createCycles() function, which:

  • Scans the DOM for elements with data-module attributes
  • Dynamically imports corresponding module files from src/modules/
  • Instantiates each component with the element and its dataset
  • Returns an array of initialized components
src/modules/cycle.ts
// Example component element
<div data-module="cycle">...</div>;
export default function (element: HTMLElement, dataset: DOMStringMap) {
// Component logic here
}

Initialization runs synchronously in the module’s default export when createCycles() executes — not in a separate hook.

  • Trigger: Called when the page is about to transition out
  • Purpose: Clean up observers, cancel animations, remove event listeners, reset state
  • Execution: Synchronous, runs during runDestroy()
  • Use Case: Essential for preventing memory leaks and cleaning up resources
onDestroy(() => {
// Stop observers
observer.destroy();
// Cancel any ongoing animations
gsap.killTweensOf(element);
// Remove event listeners
element.removeEventListener("click", handleClick);
// Reset component state
element.dataset.initialized = "false";
});

  • Trigger: Called when the page enters (after createCycles() and module bodies have run)
  • Purpose: Animate components into view, trigger entrance animations
  • Execution: Asynchronous, after module setup
  • Note: All onPageIn callbacks run in parallel using Promise.allSettled()
onPageIn(async () => {
await gsap.to(element, {
duration: 0.5,
opacity: 1,
y: 0,
ease: "power2.out",
});
});

onPageOut(fn: () => Promise<void>, options?: { element?: HTMLElement })

Section titled “onPageOut(fn: () => Promise<void>, options?: { element?: HTMLElement })”
  • Trigger: Called when the page is about to transition out
  • Purpose: Animate components out of view, prepare for transition
  • Execution: Asynchronous, runs before runDestroy()
  • Options:
    • element: If provided, the callback only runs if the element is currently visible in the viewport
  • Note: All onPageOut callbacks run in parallel using Promise.allSettled()
// Animate out only if element is visible
onPageOut(
async () => {
await gsap.to(element, {
duration: 0.3,
opacity: 0,
y: -20,
ease: "power2.in",
});
},
{ element }, // Only animate if element is in viewport
);
// Always animate out (regardless of visibility)
onPageOut(async () => {
await gsap.to(element, {
duration: 0.2,
scale: 0.8,
opacity: 0,
});
});

  • Purpose: Set up Intersection Observer for viewport detection
  • Auto-cleanup: Automatically destroyed when component is destroyed
  • Returns: Observer instance for manual control
  • Use Case: Trigger animations when elements enter/leave the viewport
const observer = onView(element, {
root: null, // Use viewport as root
rootMargin: "0px", // No margin
threshold: 0.1, // Trigger when 10% visible
autoStart: false, // Don't start automatically
once: false, // Trigger multiple times
callback: ({ isIn }) => {
if (isIn) {
// Element entered viewport
gsap.to(element, { opacity: 1, duration: 0.5 });
} else {
// Element left viewport
gsap.to(element, { opacity: 0.5, duration: 0.3 });
}
},
});
// Start the observer from the module body (when createCycles runs)
observer.start();
  • Purpose: Set up scroll tracking for scroll-based animations
  • Auto-cleanup: Automatically destroyed when component is destroyed
  • Returns: Track instance for manual control
  • Use Case: Create parallax effects, scroll-triggered animations
const track = onTrack(element, {
bounds: [0, 1], // Track from 0% to 100% of viewport
top: "center", // Start when element center hits viewport top
bottom: "center", // End when element center hits viewport bottom
callback: (value) => {
// value goes from 0 to 1 as element scrolls through viewport
gsap.set(element, {
y: value * 100, // Move element down as it scrolls
rotation: value * 360, // Rotate element as it scrolls
});
},
});

The system provides two global subscription services for handling requestAnimationFrame and resize events efficiently.

  • Purpose: Provides a centralized requestAnimationFrame loop using GSAP’s ticker
  • Data: Provides { deltaTime, time } on each frame
  • Performance: Uses GSAP’s optimized ticker for better performance
  • Use Case: Smooth animations, physics simulations, continuous updates
import { Raf } from "@lib/subs";
// Subscribe to animation frame updates
const unsubscribe = Raf.add(({ deltaTime, time }) => {
// deltaTime: time since last frame in seconds
// time: total time since start (scaled down by 0.01)
// Update element position based on time
element.style.transform = `translateX(${Math.sin(time) * 50}px)`;
// Or use deltaTime for frame-rate independent animations
element.style.opacity = Math.min(1, element.style.opacity + deltaTime);
});
// Clean up subscription
onDestroy(() => {
unsubscribe();
});
  • Purpose: Provides debounced resize events with current dimensions
  • Data: Provides { width, height } when window size changes
  • Debouncing: Automatically debounced (100ms delay) to prevent excessive updates
  • Use Case: Responsive layouts, recalculating positions, updating animations
import { Resize } from "@lib/subs";
// Subscribe to resize events
const unsubscribe = Resize.add(({ width, height }) => {
// Update component based on new dimensions
if (width < 768) {
element.classList.add("mobile");
element.classList.remove("desktop");
} else {
element.classList.add("desktop");
element.classList.remove("mobile");
}
// Recalculate positions or animations
updateLayout();
});
// Clean up subscription
onDestroy(() => {
unsubscribe();
});

Both Raf and Resize support priority-based subscription ordering:

// Higher priority (negative numbers = higher priority)
Raf.add(updateCriticalAnimation, -1);
// Normal priority (default = 0)
Raf.add(updateBackgroundAnimation, 0);
// Lower priority (positive numbers = lower priority)
Raf.add(updateNonCriticalAnimation, 1);

You can have multiple subscriptions in the same component:

export default function (element: HTMLElement, dataset: DOMStringMap) {
// Subscribe to animation frame for smooth movement
const rafUnsubscribe = Raf.add(({ time }) => {
element.style.transform = `rotate(${time * 50}deg)`;
});
// Subscribe to resize for responsive behavior
const resizeUnsubscribe = Resize.add(({ width }) => {
element.style.fontSize = width < 768 ? "14px" : "18px";
});
// Clean up both subscriptions
onDestroy(() => {
rafUnsubscribe();
resizeUnsubscribe();
});
}
  • Raf: Use sparingly - each subscription runs every frame
  • Resize: Automatically debounced, but still limit heavy operations
  • Cleanup: Always unsubscribe in onDestroy to prevent memory leaks
  • Priority: Use priorities to ensure critical updates run first

The page transition system follows this sequence:

  1. runPageOut() - Execute all onPageOut callbacks in parallel
  2. runDestroy() - Execute all onDestroy callbacks and clean up observers
  3. Scroll.toTop() - Reset scroll position
  1. createCycles() - Discover and initialize new components (module bodies run here)
  2. Scroll.resize() - Update scroll calculations
  3. runPageIn() - Execute all onPageIn callbacks in parallel

src/modules/example.ts
import { onDestroy, onPageOut, onPageIn, onView, onTrack } from "@/modules/_";
import { Raf, Resize } from "@lib/subs";
import gsap from "@lib/gsap";
export default function (element: HTMLElement, dataset: DOMStringMap) {
// Set up viewport observer
const observer = onView(element, {
threshold: 0.1,
autoStart: false,
callback: ({ isIn }) => {
if (isIn) {
element.classList.add("in-view");
} else {
element.classList.remove("in-view");
}
},
});
// Set up scroll tracking
const track = onTrack(element, {
bounds: [0, 1],
callback: (value) => {
element.style.setProperty("--scroll-progress", value.toString());
},
});
// Subscribe to animation frame for smooth effects
const rafUnsubscribe = Raf.add(({ time }) => {
if (element.classList.contains("in-view")) {
element.style.transform = `translateY(${Math.sin(time) * 5}px)`;
}
});
// Subscribe to resize for responsive behavior
const resizeUnsubscribe = Resize.add(({ width }) => {
element.style.fontSize = width < 768 ? "14px" : "18px";
});
// Initialization (runs when createCycles() loads this module)
console.log("Component initialized");
observer.start();
element.style.opacity = "0";
// Page entrance animation
onPageIn(async () => {
await gsap.to(element, {
duration: 0.5,
opacity: 1,
y: 0,
ease: "power2.out",
});
});
// Page exit animation (only if visible)
onPageOut(
async () => {
await gsap.to(element, {
duration: 0.3,
opacity: 0,
y: -20,
ease: "power2.in",
});
},
{ element },
);
// Cleanup
onDestroy(() => {
console.log("Component destroyed");
element.classList.remove("in-view");
rafUnsubscribe();
resizeUnsubscribe();
});
}

  1. Declarative: Components declare their lifecycle needs using hooks
  2. Automatic Cleanup: Observers and trackers are automatically cleaned up
  3. Performance: Only visible elements animate during page transitions
  4. Parallel Execution: Page transitions use Promise.allSettled() for efficiency
  5. Error Handling: Failed callbacks don’t block other components
  6. Flexible: Components can be as simple or complex as needed
  7. Centralized Subscriptions: Efficient handling of global events
  • Run one-time setup in the module function body (when createCycles() runs)
  • Use onDestroy for cleanup to prevent memory leaks
  • Keep setup and cleanup logic simple and focused
  • Use onPageIn/onPageOut for entrance/exit animations
  • Use the element option in onPageOut to only animate visible elements
  • Keep animations short to maintain smooth page transitions
  • Handle errors gracefully as callbacks run in parallel
  • Use onView for viewport-triggered animations
  • Use onTrack for scroll-based effects
  • Remember that observers are automatically cleaned up
  • Start observers in the module body if autoStart: false
  • Raf: Use for smooth, continuous animations or effects
  • Resize: Use for responsive behavior and layout updates
  • Cleanup: Always unsubscribe in onDestroy
  • Performance: Limit heavy operations in Raf callbacks
  • Priority: Use priorities to control execution order

The system integrates seamlessly with Taxi.js for page transitions:

  • Transition Class: Handles the coordination between Taxi.js and the lifecycle system
  • Automatic Discovery: Components are automatically found and initialized on each page
  • Smooth Transitions: Lifecycle hooks ensure proper timing of animations and cleanup