A comprehensive collection of React hooks built with useSyncExternalStore demonstrating external state management, browser API integration, and performance optimization patterns.
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
# Clone and install dependencies
npm install
# or
bun install
# Start the development server
npm run dev
# or
bun devOpen http://localhost:3000 to see all the interactive examples.
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>
);
}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" />;
}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>;
}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>
);
}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>;
}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.
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()- โค767pxuseIsTablet()- 768px-1023pxuseIsDesktop()- โฅ1024pxuseIsLarge()- โฅ1440pxuseIsDark()- Dark mode preferenceuseReducedMotion()- Reduced motion preference
CounterDispatcher- Counter controls with input fieldCounterDisplay- Read-only counter with computed propertiesFakeCursor- Custom cursor using global presenceMediaQueryDemo- Comprehensive responsive demoSimpleMediaQueryTest- Performance testing component
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
| 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.
All hooks handle server-side rendering properly:
return useSyncExternalStore(
subscribe,
() => getClientValue(), // Client snapshot
() => getServerValue() // Server snapshot - prevents hydration mismatches
);Event listeners are automatically managed:
- Shared listeners across multiple component instances
- Automatic cleanup when no components are subscribed
- Memory leak prevention
- Optimal resource usage
All hooks prevent state tearing in React's concurrent mode by using useSyncExternalStore, ensuring consistent state across all components during concurrent renders.
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
- 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
The live demo includes several interactive tests:
- Multiple components sharing the same counter state
- Real-time synchronization across displays
- Demonstrates external state management
- 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
- Responsive breakpoints - Resize to see breakpoint changes
- rAF performance test - 12 components updating simultaneously
- Accessibility support - Dark mode and reduced motion detection
- Fake cursor - Toggle to see global presence management
- Console logging - Check browser console for presence events
The demo includes a requestAnimationFrame batching test with 12 components:
- Resize your browser window rapidly
- Watch the render timestamps in all colored components
- Notice how all components update simultaneously with identical timestamps
- Compare performance with traditional event handling approaches
State tearing occurs when different components render inconsistent views of external state during React's concurrent rendering. This project demonstrates prevention through:
// Component A renders with: { count: 5 }
// External store updates to: { count: 6 }
// Component B renders with: { count: 6 }
// Result: UI shows inconsistent state (tearing)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
- Tearable Dots Demo - Interactive tearing visualization
- React UI Tearing - Comprehensive article
โ 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)
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
- useSyncExternalStore - Official React docs
- Next.js App Router - App Router guide
- Tearable Dots Demo - Interactive visualization
- React UI Tearing - Detailed explanation
- Window.matchMedia - Media query API
- RequestAnimationFrame - Animation optimization
๐ฎ Try the interactive demos - resize your window, move your mouse, and scroll to see the magic! โจ