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
useSyncExternalStoreover core'sStores, 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.