Skip to content

vinhnhq/use-sync-external-store-101

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

11 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

useSyncExternalStore 101

A comprehensive collection of React hooks built with useSyncExternalStore demonstrating external state management, browser API integration, and performance optimization patterns.

๐ŸŽฏ What This Project Demonstrates

This project showcases practical implementations of React's useSyncExternalStore hook for:

  • External state management with shared stores
  • Browser API integration (mouse, scroll, resize, media queries)
  • Performance optimization using requestAnimationFrame batching
  • Preventing state tearing in React's concurrent mode
  • SSR-safe implementations for server-side rendering
  • Global presence tracking for managing side effects

๐Ÿš€ Getting Started

# Clone and install dependencies
npm install
# or
bun install

# Start the development server
npm run dev
# or
bun dev

Open http://localhost:3000 to see all the interactive examples.

๐Ÿ“ฆ Available Hooks

๐Ÿ”ข State Management

useCounter

File: src/hooks/useCounter.tsx

Simple external counter store with increment, decrement, and reset functionality.

import useCounter, { Counter } from '@/hooks/useCounter';

function MyComponent() {
  const count = useCounter();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={Counter.increment}>+</button>
      <button onClick={Counter.decrement}>-</button>
      <button onClick={Counter.reset}>Reset</button>
    </div>
  );
}

createGlobalPresence

File: src/hooks/useGlobalPresence.tsx

Creates a hook that tracks when components are mounted/unmounted globally, useful for managing side effects.

import createGlobalPresence from '@/hooks/useGlobalPresence';

const useFakeCursorPresence = createGlobalPresence({
  onMount: () => document.body.classList.add('cursor-none'),
  onUnmount: () => document.body.classList.remove('cursor-none'),
});

function FakeCursor() {
  useFakeCursorPresence(); // Automatically manages global state
  return <div className="custom-cursor" />;
}

๐ŸŒ Browser Events

useMousePosition

File: src/hooks/useMousePosition.tsx

Tracks real-time mouse coordinates with optimized performance using requestAnimationFrame.

import useMousePosition from '@/hooks/useMousePosition';

function MouseTracker() {
  const [x, y] = useMousePosition();
  return <div>Mouse: {x}, {y}</div>;
}

useScrollY & useScrollYFloored

File: src/hooks/useScrollY.tsx

Track vertical scroll position with optional value transformation.

import { useScrollY, useScrollYFloored } from '@/hooks/useScrollY';

function ScrollTracker() {
  const scrollY = useScrollY();
  const flooredScroll = useScrollYFloored(100); // Rounds to nearest 100px

  return (
    <div>
      <p>Scroll: {scrollY}px</p>
      <p>Floored: {flooredScroll}px</p>
    </div>
  );
}

useWindowSize

File: src/hooks/useWindowSize.tsx

Monitor window dimensions with optional selector functions for value transformation.

import { useWindowSize } from '@/hooks/useWindowSize';

function WindowTracker() {
  const { width, height } = useWindowSize();
  const { width: roundedWidth } = useWindowSize({
    widthSelector: (w) => Math.round(w / 100) * 100
  });
  
  return <div>{width} ร— {height} (rounded: {roundedWidth})</div>;
}

useOnlineStatus

File: src/hooks/useOnlineStatus.tsx

Track browser online/offline status for network connectivity awareness.

import { useOnlineStatus } from '@/hooks/useOnlineStatus';

function NetworkStatus() {
  const isOnline = useOnlineStatus();
  
  return (
    <div>
      Status: {isOnline ? '๐ŸŸข Online' : '๐Ÿ”ด Offline'}
      {!isOnline && <p>Please check your internet connection</p>}
    </div>
  );
}

Note: This hook deliberately does NOT use requestAnimationFrame because online/offline events are infrequent and require immediate user feedback.

๐Ÿ“ฑ Media Queries

useMediaQuery & Convenience Hooks

File: src/hooks/useMediaQuery.tsx

Comprehensive media query system with built-in breakpoints and accessibility support.

import {
  useMediaQuery,
  useIsMobile,
  useIsDesktop,
  useCurrentBreakpoint,
  useMediaQueryState
} from '@/hooks/useMediaQuery';

function ResponsiveComponent() {
  const isMobile = useIsMobile();
  const isDark = useMediaQuery('(prefers-color-scheme: dark)');
  const breakpoint = useCurrentBreakpoint();
  const mediaState = useMediaQueryState();

  return (
    <div>
      <p>Mobile: {isMobile ? 'Yes' : 'No'}</p>
      <p>Dark mode: {isDark ? 'Yes' : 'No'}</p>
      <p>Breakpoint: {breakpoint}</p>
    </div>
  );
}

Built-in breakpoints:

  • useIsMobile() - โ‰ค767px
  • useIsTablet() - 768px-1023px
  • useIsDesktop() - โ‰ฅ1024px
  • useIsLarge() - โ‰ฅ1440px
  • useIsDark() - Dark mode preference
  • useReducedMotion() - Reduced motion preference

๐Ÿงฉ Components

Interactive Demos

  • CounterDispatcher - Counter controls with input field
  • CounterDisplay - Read-only counter with computed properties
  • FakeCursor - Custom cursor using global presence
  • MediaQueryDemo - Comprehensive responsive demo
  • SimpleMediaQueryTest - Performance testing component

๐Ÿ”ง Key Technical Features

โšก RequestAnimationFrame Optimization

Most browser event hooks use rAF for optimal performance with high-frequency events:

const notifyListeners = () => {
  requestAnimationFrame(() => {
    // All components update in the same frame
    for (const listener of listeners) {
      listener();
    }
  });
};

Benefits:

  • Eliminates layout thrashing during rapid changes
  • Batches multiple updates into single render cycles
  • Ensures updates happen at optimal rendering time
  • Improves performance with many subscribing components

๐ŸŽฏ When to Use/Skip RequestAnimationFrame

Hook Type Uses rAF? Reason
useMousePosition โœ… Yes Very high frequency (100+ events/sec)
useScrollY โœ… Yes High frequency during scrolling
useWindowSize โœ… Yes High frequency during resize
useMediaQuery โœ… Yes Multiple components benefit from batching
useOnlineStatus โŒ No Infrequent events, immediate feedback needed

Rule of thumb: Use rAF for events that fire >30 times/second or when multiple components subscribe. Skip rAF for rare events where immediate feedback is critical.

๐Ÿ›ก๏ธ SSR Safety

All hooks handle server-side rendering properly:

return useSyncExternalStore(
  subscribe,
  () => getClientValue(), // Client snapshot
  () => getServerValue()  // Server snapshot - prevents hydration mismatches
);

๐Ÿงน Automatic Cleanup

Event listeners are automatically managed:

  • Shared listeners across multiple component instances
  • Automatic cleanup when no components are subscribed
  • Memory leak prevention
  • Optimal resource usage

๐Ÿšซ State Tearing Prevention

All hooks prevent state tearing in React's concurrent mode by using useSyncExternalStore, ensuring consistent state across all components during concurrent renders.

๐Ÿ—๏ธ Project Structure

src/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ page.tsx              # Interactive demo showcase
โ”‚   โ”œโ”€โ”€ layout.tsx            # App layout
โ”‚   โ””โ”€โ”€ globals.css           # Global styles
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ CounterDispatcher.tsx # Counter with controls
โ”‚   โ”œโ”€โ”€ CounterDisplay.tsx    # Counter display
โ”‚   โ”œโ”€โ”€ FakeCursor.tsx        # Custom cursor demo
โ”‚   โ”œโ”€โ”€ MediaQueryDemo.tsx    # Media query examples
โ”‚   โ””โ”€โ”€ SimpleMediaQueryTest.tsx # Performance testing
โ””โ”€โ”€ hooks/
    โ”œโ”€โ”€ useCounter.tsx        # External counter store
    โ”œโ”€โ”€ useGlobalPresence.tsx # Global presence tracking
    โ”œโ”€โ”€ useMousePosition.tsx  # Mouse coordinate tracking
    โ”œโ”€โ”€ useScrollY.tsx        # Scroll position tracking
    โ”œโ”€โ”€ useWindowSize.tsx     # Window size tracking
    โ”œโ”€โ”€ useOnlineStatus.tsx   # Network connectivity tracking
    โ””โ”€โ”€ useMediaQuery.tsx     # Media query system

๐ŸŽจ Tech Stack

  • React 18 with useSyncExternalStore
  • Next.js 14 with App Router
  • TypeScript for complete type safety
  • Tailwind CSS for responsive styling
  • Bun for fast package management

๐Ÿงช Testing the Examples

The live demo includes several interactive tests:

1. Counter Store

  • Multiple components sharing the same counter state
  • Real-time synchronization across displays
  • Demonstrates external state management

2. Browser Events

  • Mouse tracking - Move your mouse to see coordinates
  • Scroll tracking - Scroll the page to see position updates
  • Window resize - Resize browser to see dimension changes
  • Network status - Go offline/online in dev tools to test immediate feedback

3. Media Queries

  • Responsive breakpoints - Resize to see breakpoint changes
  • rAF performance test - 12 components updating simultaneously
  • Accessibility support - Dark mode and reduced motion detection

4. Global Presence

  • Fake cursor - Toggle to see global presence management
  • Console logging - Check browser console for presence events

๐Ÿ’ก Performance Testing

The demo includes a requestAnimationFrame batching test with 12 components:

  1. Resize your browser window rapidly
  2. Watch the render timestamps in all colored components
  3. Notice how all components update simultaneously with identical timestamps
  4. Compare performance with traditional event handling approaches

โšก Understanding State Tearing

State tearing occurs when different components render inconsistent views of external state during React's concurrent rendering. This project demonstrates prevention through:

The Problem

// Component A renders with: { count: 5 }
// External store updates to: { count: 6 }
// Component B renders with: { count: 6 }
// Result: UI shows inconsistent state (tearing)

The Solution

useSyncExternalStore prevents tearing by:

  • Detecting inconsistencies during concurrent renders
  • Forcing synchronous rendering when tearing is detected
  • Ensuring consistent state snapshots across all components
  • Maintaining performance through intelligent batching

Visual Demonstrations

๐ŸŽฏ Use Cases & Patterns

When to Use These Hooks

โœ… Perfect for:

  • Browser API integration (resize, scroll, media queries)
  • Real-time data that changes frequently
  • Performance-critical subscriptions
  • Global state that needs to be SSR-safe
  • External data sources (WebSockets, local storage)

โŒ Not ideal for:

  • Simple local component state (use useState)
  • Server state (use libraries like TanStack Query)
  • Complex client state graphs (consider Zustand, Redux)

Advanced Patterns

The project demonstrates several advanced patterns:

  • Shared subscription management - Multiple components, single listener
  • Selector functions - Transform values before consumption
  • Global presence tracking - Coordinate effects across components
  • Performance optimization - rAF batching and cleanup
  • SSR compatibility - Safe server/client rendering

๐Ÿ“š Learn More

Core Documentation

Understanding State Tearing

Browser APIs


๐ŸŽฎ Try the interactive demos - resize your window, move your mouse, and scroll to see the magic! โœจ

About

use-sync-external-store-101

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors