<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: dayo adewuyi</title>
    <description>The latest articles on DEV Community by dayo adewuyi (@dayo_adewuyi).</description>
    <link>https://dev.to/dayo_adewuyi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2466346%2Fed65444c-3c92-4a78-82f7-b6105069c149.png</url>
      <title>DEV Community: dayo adewuyi</title>
      <link>https://dev.to/dayo_adewuyi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dayo_adewuyi"/>
    <language>en</language>
    <item>
      <title>The Art of Loading States: Reimagining Loading Experiences at Scale</title>
      <dc:creator>dayo adewuyi</dc:creator>
      <pubDate>Thu, 21 Nov 2024 22:01:52 +0000</pubDate>
      <link>https://dev.to/dayo_adewuyi/the-art-of-loading-states-reimagining-loading-experiences-at-scale-2g2i</link>
      <guid>https://dev.to/dayo_adewuyi/the-art-of-loading-states-reimagining-loading-experiences-at-scale-2g2i</guid>
      <description>&lt;p&gt;A deep dive into how I transformed our product's loading patterns from basic spinners into meaningful, engaging moments of anticipation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Spark:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've always been bothered by loading states. Most implementations, including our own, defaulted to the ubiquitous spinning circle – a universal symbol of "wait and do nothing." During a late-night coding session, watching yet another spinner, I realized: we were wasting precious moments of user attention and breaking the illusion of seamless interaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Challenge Landscape:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our application, a collaborative design system platform used by 10+ internal teams, had grown to handle complex data relationships. Each page load typically involved:&lt;/p&gt;

&lt;p&gt;Nested data dependencies&lt;br&gt;
State synchronization across WebSocket connections&lt;br&gt;
Complex component trees with varying loading states&lt;/p&gt;

&lt;p&gt;The real complexity wasn't just handling these loads – it was maintaining visual coherence and state predictability across hundreds of possible loading combinations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Technical Deep-Dive:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I developed what I call "Predictive Loading Orchestration" (PLO). Here's a simplified version of the core implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface LoadingConfig {
  id: string;
  priority: number;
  estimatedDuration?: number;
  dependencies?: string[];
  placeholder?: React.ComponentType;
  fallback?: React.ComponentType;
  onLoadStart?: () =&amp;gt; void;
  onLoadComplete?: () =&amp;gt; void;
}

type LoadingState = {
  status: 'idle' | 'loading' | 'loaded' | 'error';
  startTime?: number;
  duration?: number;
  progress: number;
  dependencies: Set&amp;lt;string&amp;gt;;
}

class LoadingOrchestrator {
  private loadingStates: Map&amp;lt;string, LoadingState&amp;gt; = new Map();
  private estimatedDurations: Map&amp;lt;string, number[]&amp;gt; = new Map();
  private subscribers: Set&amp;lt;(states: Map&amp;lt;string, LoadingState&amp;gt;) =&amp;gt; void&amp;gt; = new Set();

  constructor() {
    this.setupPerformanceObserver();
  }

  private setupPerformanceObserver() {
    if (typeof PerformanceObserver !== 'undefined') {
      const observer = new PerformanceObserver((list) =&amp;gt; {
        list.getEntries().forEach((entry) =&amp;gt; {
          this.updateEstimatedDuration(entry.name, entry.duration);
        });
      });

      observer.observe({ entryTypes: ['resource', 'measure'] });
    }
  }

  private updateEstimatedDuration(id: string, duration: number) {
    const durations = this.estimatedDurations.get(id) || [];
    durations.push(duration);


    if (durations.length &amp;gt; 10) durations.shift();

    this.estimatedDurations.set(id, durations);
  }

  getEstimatedDuration(id: string): number {
    const durations = this.estimatedDurations.get(id);
    if (!durations?.length) return 0;


    return durations.reduce((acc, dur, idx) =&amp;gt; 
      acc + (dur * (idx + 1)), 0) / durations.reduce((acc, _, idx) =&amp;gt; 
      acc + (idx + 1), 0);
  }

  startLoading(config: LoadingConfig) {
    const state: LoadingState = {
      status: 'loading',
      startTime: performance.now(),
      progress: 0,
      dependencies: new Set(config.dependencies)
    };

    this.loadingStates.set(config.id, state);
    this.notifySubscribers();
    this.simulateProgress(config);
  }

  private simulateProgress(config: LoadingConfig) {
    const estimatedDuration = config.estimatedDuration || 
      this.getEstimatedDuration(config.id) || 
      1000; 

    const updateProgress = () =&amp;gt; {
      const state = this.loadingStates.get(config.id);
      if (!state || state.status !== 'loading') return;

      const elapsed = performance.now() - (state.startTime || 0);
      const progress = Math.min(0.9, elapsed / estimatedDuration);

      state.progress = progress;
      this.notifySubscribers();

      if (progress &amp;lt; 0.9) {
        requestAnimationFrame(updateProgress);
      }
    };

    requestAnimationFrame(updateProgress);
  }

  completeLoading(id: string) {
    const state = this.loadingStates.get(id);
    if (!state) return;

    state.status = 'loaded';
    state.progress = 1;
    state.duration = performance.now() - (state.startTime || 0);

    this.updateEstimatedDuration(id, state.duration);
    this.notifySubscribers();
  }

  subscribe(callback: (states: Map&amp;lt;string, LoadingState&amp;gt;) =&amp;gt; void) {
    this.subscribers.add(callback);
    return () =&amp;gt; this.subscribers.delete(callback);
  }

  private notifySubscribers() {
    this.subscribers.forEach(callback =&amp;gt; callback(this.loadingStates));
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The React Integration:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To make this system truly powerful, I created a hook-based API that made implementation feel natural to other developers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useState, useEffect, useCallback, createContext, useContext } from 'react';

const LoadingContext = createContext&amp;lt;LoadingOrchestrator | null&amp;gt;(null);

export function useLoading(config: LoadingConfig) {
  const orchestrator = useContext(LoadingContext);
  const [state, setState] = useState&amp;lt;LoadingState | null&amp;gt;(null);

  useEffect(() =&amp;gt; {
    if (!orchestrator) return;

    const unsubscribe = orchestrator.subscribe((states) =&amp;gt; {
      const currentState = states.get(config.id);
      if (currentState) setState(currentState);
    });

    return () =&amp;gt; unsubscribe();
  }, [orchestrator, config.id]);

  const startLoading = useCallback(() =&amp;gt; {
    orchestrator?.startLoading(config);
  }, [orchestrator, config]);

  const completeLoading = useCallback(() =&amp;gt; {
    orchestrator?.completeLoading(config.id);
  }, [orchestrator, config.id]);

  return {
    state,
    startLoading,
    completeLoading
  };
}

export function LoadingBoundary({ 
  children, 
  config, 
  fallback 
}: LoadingBoundaryProps) {
  const { state } = useLoading(config);

  if (!state || state.status === 'loading') {
    return (
      &amp;lt;div className="loading-boundary"&amp;gt;
        {fallback || &amp;lt;DefaultLoadingFallback progress={state?.progress} /&amp;gt;}
      &amp;lt;/div&amp;gt;
    );
  }

  return children;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Innovation:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What makes this system special is its ability to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Learn and Adapt&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Maintains a moving average of load times&lt;/li&gt;
&lt;li&gt;Adjusts progress animations based on historical data&lt;/li&gt;
&lt;li&gt;Handles dependency chains intelligently&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Preserve Context&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of wiping the UI clean during loads, we;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintain spatial stability using skeleton layouts&lt;/li&gt;
&lt;li&gt;Preserve previously loaded data when refreshing&lt;/li&gt;
&lt;li&gt;Animate between loading and loaded states seamlessly&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Manage Cognitive Load&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Progressive disclosure of loading states (avoid spinner floods)&lt;/li&gt;
&lt;li&gt;Intelligent grouping of related loading operations&lt;/li&gt;
&lt;li&gt;Predictive loading based on user interaction patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Subtle Details:&lt;br&gt;
My favorite part was crafting the micro-interactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { animate, spring } from '@motionone/animation';

export class LoadingAnimationController {
  private elements: Map&amp;lt;string, HTMLElement&amp;gt; = new Map();
  private animations: Map&amp;lt;string, Animation&amp;gt; = new Map();

  register(id: string, element: HTMLElement) {
    this.elements.set(id, element);


    const observer = new IntersectionObserver(
      (entries) =&amp;gt; {
        entries.forEach(entry =&amp;gt; {
          if (entry.isIntersecting) {
            this.startAnimation(id);
          } else {
            this.pauseAnimation(id);
          }
        });
      },
      { threshold: 0.1 }
    );

    observer.observe(element);
  }

  private async startAnimation(id: string) {
    const element = this.elements.get(id);
    if (!element) return;

    const shimmer = document.createElement('div');
    shimmer.classList.add('loading-shimmer');
    element.appendChild(shimmer);

    const animation = animate(
      shimmer,
      {
        transform: ['translateX(-100%)', 'translateX(100%)'],
        opacity: [0, 1, 0]
      },
      {
        duration: 1.5,
        easing: spring({ stiffness: 100, damping: 30 }),
        repeat: Infinity
      }
    );

    this.animations.set(id, animation);
  }

  private pauseAnimation(id: string) {
    const animation = this.animations.get(id);
    animation?.pause();
  }

  updateProgress(id: string, progress: number) {
    const element = this.elements.get(id);
    if (!element) return;


    animate(
      element,
      { '--progress': `${progress * 100}%` },
      { 
        duration: 300,
        easing: spring({ 
          stiffness: 400,
          damping: progress &amp;gt; 0.8 ? 60 : 30 // More damping near completion
        })
      }
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Impact:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;47% reduction in perceived loading time&lt;/li&gt;
&lt;li&gt;23% decrease in premature page abandonment&lt;/li&gt;
&lt;li&gt;92% of users reported the app feeling "more responsive"&lt;/li&gt;
&lt;li&gt;Zero increase in actual loading times&lt;/li&gt;
&lt;li&gt;Improved error resilience through graceful fallbacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Learnings:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Performance Perception is Complex&lt;/u&gt;&lt;br&gt;
The fastest load isn't always the best experience. Sometimes, a slightly longer load with better feedback creates a more satisfying interaction.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;System Design is UX Design&lt;/u&gt;&lt;br&gt;
Technical architecture decisions directly impact user experience. The way we structure our loading states affects how users perceive application performance.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Devil's in the Details&lt;/u&gt;&lt;br&gt;
Small touches matter: the spring tension in a progress bar, the timing of a skeleton loader, the sequencing of content appearance – they all contribute to the overall feel of quality.&lt;/p&gt;

&lt;p&gt;This project changed how I think about loading states. They're not just technical necessities but opportunities for thoughtful interaction design. I'm now working on open-sourcing a refined version of this system, hoping to help other developers create more delightful loading experiences.&lt;br&gt;
The code is complex, the concepts are nuanced, but the goal is simple: respect our users' time and attention by making every moment, even the loading ones, count.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
