Skip to main content

defineChannel

defineChannel binds a channel's name and message map once, at module level, and hands back ready-to-use typed hooks. It's sugar over useChannel + useMessage + useSend for when the trio feels like ceremony: declare the channel in one file, and every component gets two-line usage with no generics and no name strings to repeat.

shop-channel.ts
import { defineChannel } from 'use-everywhere';

type ShopEvents = { 'cart-updated': { items: number } };

export const shop = defineChannel<ShopEvents>('shop');
CartBadge.tsx
import { useState } from 'react';
import { shop } from './shop-channel';

function CartBadge() {
const [items, setItems] = useState(0);
const send = shop.useSend();

shop.useMessage('cart-updated', (p) => setItems(p.items)); // other tabs

const addToCart = () => {
setItems(items + 1); // 1. this tab, explicitly
send('cart-updated', { items: items + 1 }); // 2. every other tab
};

return <button onClick={addToCart}>Cart ({items})</button>;
}

Compare with the same component built from the trio: the behavior is identical — this version just moved the name and the type parameter out of the component and into the channel's own module.

Signature

function defineChannel<M extends MessageMap>(name: string): ChannelHooks<M>;

Not a hook — a plain factory, meant to run at module scope. It lives in the React package only (use-everywhere, not @use-everywhere/core), because what it returns is hooks.

Return value

MemberTypeWhat it is
useSend() => (type, payload) => voidThe bound channel's post, stable identity — same contract as useSend.
useMessage(type, handler) => voidSubscribe to one event type — same freshness contract as useMessage.
get() => Channel<M>The underlying channel instance, for code outside React (module-level handlers, workers, tests).

Everything stays shared

defineChannel creates no new machinery. It resolves to the same page-wide channel singleton the standalone hooks use, so all of these are on one wire and can be mixed freely:

const bound = defineChannel<ShopEvents>('shop');

bound.get() === getChannel('shop'); // same instance
useChannel<ShopEvents>('shop'); // same instance, in a component
defineChannel<ShopEvents>('shop').get(); // same instance again

Two modules calling defineChannel('shop') independently talk to each other — the name is still the identity, exactly like everywhere else in the library.

Gotchas

  • Call the returned hooks like hooks. shop.useSend() and shop.useMessage(...) follow the Rules of Hooks — top level of a component, unconditionally. The namespace.useX(...) call shape is fully understood by eslint-plugin-react-hooks.
  • Don't rename them during destructuring. const { useMessage } = shop is fine; const { useMessage: onCart } = shop hides the hook from the linter. Calling through the namespace (shop.useMessage) sidesteps the question entirely.
  • Same semantics as the trio. No echo to the sender, no history, at-most-once delivery — everything on the useMessage and useSend pages applies unchanged.

Where to next