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.
Overview
Section titled “Overview”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.
Component Discovery
Section titled “Component Discovery”Components are automatically discovered using the createCycles() function, which:
- Scans the DOM for elements with
data-moduleattributes - Dynamically imports corresponding module files from
src/modules/ - Instantiates each component with the element and its dataset
- Returns an array of initialized components
// Example component element<div data-module="cycle">...</div>;
export default function (element: HTMLElement, dataset: DOMStringMap) { // Component logic here}Pure Lifecycle Hooks
Section titled “Pure Lifecycle Hooks”Initialization runs synchronously in the module’s default export when createCycles() executes — not in a separate hook.
onDestroy(fn: () => void)
Section titled “onDestroy(fn: () => void)”- 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";});Page Transition Hooks
Section titled “Page Transition Hooks”onPageIn(fn: () => Promise<void>)
Section titled “onPageIn(fn: () => Promise<void>)”- 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
onPageIncallbacks run in parallel usingPromise.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
onPageOutcallbacks run in parallel usingPromise.allSettled()
// Animate out only if element is visibleonPageOut( 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, });});Animation Utilities
Section titled “Animation Utilities”onView(element, config)
Section titled “onView(element, config)”- 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();onTrack(element, config)
Section titled “onTrack(element, config)”- 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 }); },});Subscription System (Raf & Resize)
Section titled “Subscription System (Raf & Resize)”The system provides two global subscription services for handling requestAnimationFrame and resize events efficiently.
Raf - Request Animation Frame
Section titled “Raf - Request Animation Frame”- 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 updatesconst 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 subscriptiononDestroy(() => { unsubscribe();});Resize - Window Resize Events
Section titled “Resize - Window Resize Events”- 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 eventsconst 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 subscriptiononDestroy(() => { unsubscribe();});Advanced Subscription Usage
Section titled “Advanced Subscription Usage”Priority System
Section titled “Priority System”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);Multiple Subscriptions
Section titled “Multiple Subscriptions”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(); });}Performance Considerations
Section titled “Performance Considerations”- Raf: Use sparingly - each subscription runs every frame
- Resize: Automatically debounced, but still limit heavy operations
- Cleanup: Always unsubscribe in
onDestroyto prevent memory leaks - Priority: Use priorities to ensure critical updates run first
Page Transition Flow
Section titled “Page Transition Flow”The page transition system follows this sequence:
Page Exit (transitionOut)
Section titled “Page Exit (transitionOut)”runPageOut()- Execute allonPageOutcallbacks in parallelrunDestroy()- Execute allonDestroycallbacks and clean up observersScroll.toTop()- Reset scroll position
Page Enter (transitionIn)
Section titled “Page Enter (transitionIn)”createCycles()- Discover and initialize new components (module bodies run here)Scroll.resize()- Update scroll calculationsrunPageIn()- Execute allonPageIncallbacks in parallel
Complete Component Example
Section titled “Complete Component Example”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(); });}Key Benefits
Section titled “Key Benefits”- Declarative: Components declare their lifecycle needs using hooks
- Automatic Cleanup: Observers and trackers are automatically cleaned up
- Performance: Only visible elements animate during page transitions
- Parallel Execution: Page transitions use
Promise.allSettled()for efficiency - Error Handling: Failed callbacks don’t block other components
- Flexible: Components can be as simple or complex as needed
- Centralized Subscriptions: Efficient handling of global events
Best Practices
Section titled “Best Practices”Lifecycle Hooks
Section titled “Lifecycle Hooks”- Run one-time setup in the module function body (when
createCycles()runs) - Use
onDestroyfor cleanup to prevent memory leaks - Keep setup and cleanup logic simple and focused
Page Transitions
Section titled “Page Transitions”- Use
onPageIn/onPageOutfor entrance/exit animations - Use the
elementoption inonPageOutto only animate visible elements - Keep animations short to maintain smooth page transitions
- Handle errors gracefully as callbacks run in parallel
Animation Utilities
Section titled “Animation Utilities”- Use
onViewfor viewport-triggered animations - Use
onTrackfor scroll-based effects - Remember that observers are automatically cleaned up
- Start observers in the module body if
autoStart: false
Subscription System
Section titled “Subscription System”- 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
Integration with Taxi.js
Section titled “Integration with Taxi.js”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
Related
Section titled “Related”- Module system — discovery and DOM wiring
- Scroll system — Lenis and scroll subscriptions
- Observer & track — detailed
onView/onTrackAPI - Subscriptions —
RafandResizeservices