npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

use-tab-sync

v5.0.0

Published

Zero deps. Minimal. Cross-tab state sync for React.

Readme

use-tab-sync

Zero deps. Minimal. Cross-tab state sync for React.

One hook. All tabs. Instant sync. No server. No boilerplate.

npm install use-tab-sync
import { useTabSync } from 'use-tab-sync';

function Cart() {
  const [items, setItems] = useTabSync('cart', []);

  return (
    <button onClick={() => setItems(prev => [...prev, { id: Date.now() }])}>
      Add ({items.length})
    </button>
  );
}

Open two tabs. Click in one — the other updates instantly.

Why

| Without | With | |---------|------| | BroadcastChannel setup | 1 line | | localStorage fallback | 1 line | | SSR hydration handling | 1 line | | Self-loop prevention | Automatic | | Functional updates | Built-in |

~50 lines of boilerplate → 1 hook call.

Hooks

| Hook | Purpose | |------|---------| | useTabSync | Cross-tab state sync (BroadcastChannel + localStorage) | | useTabSyncReducer | Cross-tab reducer pattern | | useTabStorage | localStorage-only with TTL (no cross-tab sync) |

API

useTabSync<T>(key, defaultValue, options?)

Drop-in replacement for useState that syncs across tabs.

const [state, setState] = useTabSync('key', defaultValue);

Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | channel | string | 'use-tab-sync' | BroadcastChannel namespace | | prefix | string | 'rts:' | localStorage key prefix | | persist | boolean | true | Persist across page reloads | | sync | (keyof T)[] | undefined | Selective field sync | | serializer | { serialize, deserialize } | JSON | Custom serialization | | conflict | 'lastWriteWins' \| 'merge' \| (local, remote) => T | 'lastWriteWins' | Conflict resolution | | onSync | (event: SyncEvent<T>) => void | undefined | Sync event callback | | ttl | number | undefined | Time-to-live in ms | | onExpire | (key, value) => void | undefined | Expiration callback | | maxSize | number | undefined | Max payload size in bytes | | onSizeExceeded | (key, size, limit) => void | undefined | Size exceeded callback |

useTabSyncReducer(reducer, initialState, key, options?)

Cross-tab reducer pattern — dispatch actions that sync across all tabs.

import { useTabSyncReducer } from 'use-tab-sync';

function reducer(state: string[], action: { type: string; item?: string }) {
  switch (action.type) {
    case 'add': return [...state, action.item!];
    case 'clear': return [];
    default: return state;
  }
}

function Cart() {
  const [items, dispatch] = useTabSyncReducer(reducer, [], 'cart');

  return (
    <button onClick={() => dispatch({ type: 'add', item: 'Shoe' })}>
      Add Item
    </button>
  );
}

useTabStorage<T>(key, defaultValue, options?)

localStorage-only hook with TTL. No cross-tab sync — use for persistent local state.

import { useTabStorage } from 'use-tab-sync';

function Preferences() {
  const [theme, setTheme, removeTheme] = useTabStorage('theme', 'light', {
    ttl: 24 * 60 * 60 * 1000 // 24 hours
  });

  return (
    <div>
      <p>Theme: {theme}</p>
      <button onClick={() => setTheme('dark')}>Dark</button>
      <button onClick={removeTheme}>Reset</button>
    </div>
  );
}

Options

Selective Sync

Only sync specific fields — other fields stay local.

const [user, setUser] = useTabSync('user', { name: 'John', localPref: 'x' }, {
  sync: ['name'] // only 'name' syncs across tabs
});

Remote updates merge into local state — they don't replace it.

Custom Serializer

Handle non-JSON types (Date, Map, Set, etc).

const [date, setDate] = useTabSync('lastSeen', new Date(), {
  serializer: {
    serialize: (d) => d.toISOString(),
    deserialize: (s) => new Date(s)
  }
});

Ephemeral State

Sync across tabs without persisting to localStorage.

const [hovered, setHovered] = useTabSync<string | null>('hovered', null, {
  persist: false
});

Channel Namespacing

Isolate different features to prevent cross-talk.

const [theme, setTheme] = useTabSync('theme', 'light', { channel: 'settings' });
const [cart, setCart] = useTabSync('cart', [], { channel: 'shop' });

Conflict Resolution

Control how simultaneous updates from multiple tabs are resolved.

// Default: last write wins (timestamp + tab order)
const [state, setState] = useTabSync('key', value, {
  conflict: 'lastWriteWins'
});

// Shallow merge objects
const [settings, setSettings] = useTabSync('settings', defaults, {
  conflict: 'merge'
});

// Custom resolver
const [data, setData] = useTabSync('data', initial, {
  conflict: (local, remote) => {
    return (local?.length ?? 0) > (remote?.length ?? 0) ? local : remote;
  }
});

TTL (Time-to-Live)

Auto-expire state after a duration.

const [token, setToken] = useTabSync('auth_token', null, {
  ttl: 60 * 60 * 1000, // 1 hour
  onExpire: (key, value) => {
    console.log(`Token expired: ${key}`);
    // Redirect to login, etc.
  }
});

Size Limits

Get notified when payloads exceed a threshold.

const [data, setData] = useTabSync('large_data', [], {
  maxSize: 10 * 1024, // 10KB
  onSizeExceeded: (key, size, limit) => {
    console.warn(`${key} is ${(size/1024).toFixed(1)}KB, limit is ${(limit/1024).toFixed(1)}KB`);
  }
});

Sync Events (onSync)

Monitor every sync event for analytics, debugging, or side effects.

const [state, setState] = useTabSync('key', value, {
  onSync: ({ key, value, direction, tabId, timestamp }) => {
    console.log(`[${direction}] ${key}:`, value);
  }
});

Features

  • Zero dependencies — React only
  • Tree-shakable — ESM + CJS
  • SSR-safe — Next.js App Router compatible
  • TypeScript — full generics
  • Functional updatessetState(prev => prev + 1)
  • Self-loop prevention — sender ignores own broadcasts
  • Conflict resolution — lastWriteWins, merge, or custom
  • TTL/expiration — auto-expire state
  • Size limits — warn on large payloads
  • Sync events — onSync callback
  • Dev warnings — rapid updates, large payloads

How

  1. localStorage for persistence
  2. BroadcastChannel for instant sync
  3. storage event fallback for older browsers
  4. useSyncExternalStore for SSR safety

License

MIT