JS SDK — React (@loopchat/react)

Hooks + headless-first components + a styled default chat UI on top of @loopchat/core.

  • Repo: js-sdk/packages/react.
  • Dependencies: @loopchat/core (workspace). Peer: react ^18 || ^19.
  • Build: tsup → ESM + CJS + .d.ts. Ships one small stylesheet (@loopchat/react/styles.css), overridable via class names — no CSS framework.
  • All hooks are built on useSyncExternalStore over core's Stores, which is what keeps this package thin.
import { Chat, Channel, ChannelList, MessageList, MessageInput, Filter } from '@loopchat/react';
import '@loopchat/react/styles.css';

<Chat client={client}>
  <ChannelList filter={Filter.in('members', [userId])} onSelect={setActive} activeChannel={active} />
  <Channel channel={active}>
    <MessageList />
    <MessageInput />
  </Channel>
</Chat>

A usable chat screen fits in under 50 lines — see js-sdk/examples/react-demo (Vite).

Providers

Component Props Description
<Chat> { client: LoopChatClient, children } Provides the client to descendant hooks/components (context).
<Channel> { channel: CoreChannel, children } Provides a channel to descendant hooks/components. Note: here Channel is the provider component; the Channel class stays in @loopchat/core.

useChatClient() and useChannel() read those contexts and throw a descriptive error when used outside the matching provider.

Hooks

Hook Returns
useStore(store) Snapshot of any core Store — the primitive all other hooks build on.
useMessages() Live readonly MessageObject[] of the enclosing <Channel>.
useMembers() Live readonly MemberObject[] of the enclosing <Channel>.
useCurrentUser() UserObject | null (null until connectUser resolves).
useUnreadCount() Total unread count across all channels (for badges).
useChannelUnreadCount() Unread count of the enclosing <Channel>.
useConnectionStatus() 'connecting' | 'connected' | 'disconnected'.
useChannelList(filter, sort?, options?) { channels, loading, error, refresh } — see below.

useChannelList

const { channels, loading, error, refresh } = useChannelList(
  Filter.in('members', [userId]),
  [{ field: 'last_message_at' }],   // default sort
  { limit: 30 },                    // QueryChannelsOptions; watch: true is applied by default
);

Queries (and watches) the channel list and keeps it live: new message.new / notification.message_new events move the affected channel to the top (or trigger a re-query if the channel isn't loaded yet), channel.deleted removes it, channel.created re-queries. The query re-runs only when the content of filter/sort/options changes (deep-compared via JSON), and refresh() forces a re-query (e.g. pull-to-refresh).

Styled components

All accept className and use lc-* class names from styles.css.

Component Props Behavior
<ChannelList> filter, sort?, options?, onSelect?(channel), activeChannel?, className? Styled channel list: avatar, name, last-message preview, unread badge; live via useChannelList; loading/error/empty states.
<MessageList> onMessageClick?(message), className? Scrollable list bound to the enclosing <Channel>: newest at the bottom, own messages right-aligned, sender name + avatar for others, day separators, scroll-to-top pagination (via channel.query()), sticks to the bottom on new messages, and marks the channel read while on screen with unread messages.
<MessageInput> onMessageSent?(message), placeholder?, disabled?, className? Composer bound to the enclosing <Channel>: send on Enter (Shift+Enter for newline), send button with disabled/sending states, inline error display.
<Avatar> name, image?, className? Small round avatar: image when available, initials otherwise (initialsOf).

Re-exports

For single-import convenience, the most-used core surface is re-exported: LoopChatClient, Filter, devToken, LoopChatNetworkError, Store, and common types (ChannelExtraData, ChannelObject, ClientEvent, ConnectionStatus, DeviceObject, FilterObject, MemberObject, MessageObject, QueryChannelsOptions, SendMessageInput, SortOption, UserObject).

Testing

@testing-library/react + vitest (jsdom) on hooks and components against fake core stores; the Vite example app is the in-browser verification target against a live server.