react-native-soft-chat
v0.1.1
Published
A customizable React Native chat UI toolkit, built on MIT-licensed open-source chat components — theming, i18n/RTL, streaming AI messages, location cards, and full media playback, with an emphasis on type safety, performance, and customization
Maintainers
Readme
React Native Soft Chat
A customizable React Native chat UI toolkit based on MIT-licensed open-source chat components,
with additional improvements and a modernized developer experience — one <SoftChat /> component
with a fully typed API, render props for every visual piece, and the animated FlatList
performance work (day labels, scroll-to-bottom, inverted lists) already done for you.
react-native-soft-chat is an independently developed package built on source originally
from react-native-gifted-chat. It is
not the official react-native-gifted-chat package, and it is not maintained by that
project's authors — see License for the required attribution.
Installation
npm install react-native-soft-chatyarn add react-native-soft-chatpnpm add react-native-soft-chatreact-native-soft-chat also depends on a few native modules that most React Native projects
already have, or that you'll need to install and configure per their own setup guides:
npm install react-native-gesture-handler react-native-reanimated react-native-keyboard-controller react-native-safe-area-contextBasic example
import React, { useCallback, useState } from 'react'
import { SoftChat, ChatMessage, ChatUser } from 'react-native-soft-chat'
const currentUser: ChatUser = { _id: 1, name: 'You' }
export default function ChatScreen () {
const [messages, setMessages] = useState<ChatMessage[]>([])
const onSend = useCallback((newMessages: ChatMessage[]) => {
setMessages(previous => SoftChat.append(previous, newMessages))
}, [])
return (
<SoftChat
messages={messages}
user={currentUser}
onSend={onSend}
/>
)
}More scenarios — custom bubbles, custom input, avatars, theming, message types, pagination,
empty states, message actions, and TypeScript usage — are in examples/.
Features
- Typed data model —
ChatMessageandChatUserare plain interfaces you can extend with your own fields (delivery receipts, attachments, anything) and keep full type safety through every render prop and callback. - Render props for everything — bubbles, avatars, the composer, send button, timestamps, system messages, day separators, and the input toolbar can each be swapped out independently.
- A full design-token theme system — override colors, radii, spacing, and typography via
theme/darkTheme, with runtime light/dark switching. - Streaming (AI) messages — flag a message
streaming: truefor a live typing caret and automatic markdown rendering, driven by the bundleduseStreamingMessageshook. - Media messages — text, image, video, and audio messages with real inline playback (via the
optional
expo-video/expo-audiopeers) and a tappable fallback card when they're absent, plus a pinch-to-zoom image viewer out of the box. - Location messages — any message with a
location: { latitude, longitude }renders a map card that opens the system maps app on tap — no extra dependency required. - Quick replies — inline suggested-reply chips with radio or checkbox selection.
- Swipe-to-reply, emoji reactions, and a long-press context menu — optional, each with their own render props for the preview, the picker, and the action list.
- Pagination —
loadEarlierMessagesPropsdrives both a manual "load earlier" button and infinite scroll-up, with an optional@shopify/flash-listengine swap for very long histories. - Animated day separators, scroll-to-bottom, and typing indicator built on Reanimated, tuned to avoid re-rendering the message list on every scroll frame.
- Internationalization & RTL — 15 bundled UI translations, Day.js date localization, and layout mirroring for right-to-left languages.
- Light/dark theming via a
colorSchemeoverride, independent of the OS setting. - Keyboard handling via
react-native-keyboard-controller, with an escape hatch (disableKeyboardProvider) for apps that already mount their own provider.
Customization
Every built-in piece is replaceable through a render* prop on <SoftChat />. Each render prop
receives the same props the built-in component would have, so you can also just re-render the
built-in component with a subset of props overridden (see
examples/05-theme-customization.tsx).
| Area | Prop | Renders |
| --- | --- | --- |
| Messages | renderBubble | ChatBubble — the message container (text/image/video/audio/custom view) |
| | renderMessageText / renderMessageImage / renderMessageVideo / renderMessageAudio | Individual message content |
| | renderCustomView | Extra content inside a bubble (isCustomViewBottom controls placement) |
| | renderSystemMessage | Centered system notices (message.system: true) |
| | renderTime | The per-message timestamp |
| | renderDay | The date separator between days |
| Avatars | renderAvatar | Per-message avatar (pass null to disable entirely) |
| Input | renderInputToolbar | The whole composer bar (component, element, or function) |
| | renderComposer | Just the text field |
| | renderActions | The leading action button (attachments, etc.) |
| | renderSend | The send button |
| | renderAccessory | An optional second row below the composer |
| Quick replies | renderQuickReplies / renderQuickReplySend | Suggested-reply chips and their send affordance |
| List | renderChatEmpty | Empty-state content |
| | renderFooter / renderChatFooter | Typing indicator area / content below the list |
| | renderLoadEarlier | The "load earlier messages" control |
| Reactions | reactions.renderReactions / reactions.renderReactionPicker | Reaction pills and the long-press picker |
| Reply | reply.renderPreview / reply.renderMessageReply | The input-toolbar reply preview and the inline quoted-message |
Styling that doesn't need custom markup is usually available as a style prop directly
(imageStyle, timeTextStyle, quickReplyStyle, typingIndicatorStyle, …); anything not
exposed as a direct prop can be reached by re-rendering the relevant built-in component (e.g.
ChatBubble's wrapperStyle/textStyle) from inside a render* prop.
Theming
Every built-in component reads its colors, radii, spacing, and typography from a single design
token object, resolved once per render and shared through context — so switching theme,
darkTheme, or the system color scheme updates every component at once:
<SoftChat
theme={{ colors: { accent: '#3390EC', outgoingBubble: '#EFFEDE' } }}
darkTheme={{ colors: { background: '#0E1621' } }}
{...props}
/>theme applies in light mode, darkTheme in dark mode; each is deep-merged over the built-in
defaults, so you only need to specify the tokens you want to change. Reach the resolved theme
from your own components with useTheme() / useThemedStyles(factory). Icons (send, mic,
check, …) can be overridden the same way via the icons prop — pass your own icon library and
the built-in glyph is used for anything you don't override.
Streaming (AI) messages
Mark a message streaming: true to show a live typing caret, and pair it with the bundled
useStreamingMessages hook to manage the state:
import { SoftChat, useStreamingMessages } from 'react-native-soft-chat'
const { messages, append, startStream } = useStreamingMessages()
const handleSend = useCallback(async (newMessages) => {
append(newMessages)
const stream = startStream({ user: assistant })
for await (const chunk of fetchAssistantReply(stream.signal))
stream.push(chunk)
stream.done()
}, [append, startStream])
<SoftChat messages={messages} onSend={handleSend} user={currentUser} />Streaming messages render as markdown automatically — headings, lists, code blocks, and links
are handled by a built-in dependency-free renderer, or by the optional react-native-streamdown
peer (better handling of incomplete, mid-stream markdown) when it's installed.
Internationalization & RTL
Pass a locale (e.g. 'es', 'pt-BR') to switch both the 15 bundled UI translations and the
Day.js date formatting; override any string directly with labels. Right-to-left languages
(Arabic, Hebrew, Persian, Urdu, …) are detected automatically from locale and mirror this
SoftChat instance's own layout — pass forceRTL to override the detection. This never touches
React Native's global I18nManager, which is an app-wide, native-reload-requiring switch.
TypeScript
Every component, prop bag, and callback is fully typed and exported:
import type {
ChatMessage,
ChatUser,
SoftChatProps,
ChatBubbleProps,
ChatComposerProps,
ChatAvatarProps,
QuickReplyOption,
MessageReaction,
MessageMenuItem,
ChatTheme,
ChatIcons,
ChatLabels,
} from 'react-native-soft-chat'SoftChat is generic over your message type — extend ChatMessage to add your own fields and
every render prop and callback narrows to your extended type automatically. See
examples/10-typescript-usage.tsx.
Performance
- Keep
messagesa new array reference only when it actually changes;SoftChat.append/SoftChat.prependreturn a new array without mutating the one you pass in. - The message list is inverted (newest message at index 0) by default, which lets the FlatList
render new messages without re-measuring everything above them — avoid setting
isInverted={false}unless you have a specific reason to. - Prefer stamping stable, unique
_ids (e.g. from your backend) over regenerating them on every render; unstable ids defeat the list's item recycling. - Day separators are computed off the JS thread with Reanimated; disable them
(
isDayAnimationEnabled={false}) only if you've profiled and found they matter for your case. - Memoize
render*props that close over component state (as the examples do withuseCallback) so the list doesn't re-render every message row on unrelated state changes. - For very long histories, install
@shopify/flash-listand passisFlashListEnabledto swap the list's rendering engine; the message list falls back toFlatList(with a dev warning) when the package isn't installed, so it's always safe to leave the prop set.
Accessibility
- The composer and send button carry
accessibilityRoleandaccessibilityLabelvalues out of the box; avatars are marked withaccessibilityRole="image". - The "load earlier messages" control exposes
accessibilityRole="button"and disables itself (rather than hiding) while loading, so screen reader users get consistent feedback. - Custom
render*components are your responsibility to make accessible the same way you would any other React Native view — the props passed in (e.g.currentMessage) give you everything needed to build meaningful labels.
Compatibility with react-native-gifted-chat
This package deliberately mirrors react-native-gifted-chat's
public API wherever the functionality is the same, so it should feel immediately familiar. It is
not a drop-in replacement — the main component is SoftChat, not GiftedChat, and a handful
of names differ for concrete reasons — but most <GiftedChat /> code ports over by changing only
the import and the component name:
- Every
SoftChatPropsfield that has aGiftedChatPropsequivalent uses the same name and signature —messages,user,onSend,locale,isTyping,isInverted,isSendButtonAlwaysVisible, everyrender*prop (renderBubble,renderAvatar,renderComposer,renderInputToolbar,renderMessageImage,renderMessageVideo,renderMessageAudio,renderCustomView,renderTime,renderDay,renderSystemMessage,renderQuickReplies,renderSend,renderActions,renderAccessory,renderChatFooter,renderUsername,renderLoading, …), and every callback (onPressAvatar,onLongPressAvatar,onPressMessage,onLongPressMessage,onQuickReply,onPressActionButton,messageIdGenerator), keep their original name and parameters. - The message and user models match —
ChatMessage/ChatUserhave the exact same fields asIMessage/User(_id,text,createdAt,user,image,video,audio,system,sent,received,pending,quickReplies, …), plus additive fields for this package's own features (streaming,duration,videoNote,reactions,replyMessage).IMessage,IChatMessage,User,Reply, andQuickRepliesare exported as type aliases ofChatMessage/ChatUser/QuickReplyOption/QuickRepliesPayload, so existing type annotations written against the original package's types keep working unchanged.
Where this package intentionally differs:
| Difference | Reason |
| --- | --- |
| Main component is SoftChat, not GiftedChat | This package's identity — not a functional difference. |
| Canonical types are ChatMessage/ChatUser, not IMessage/User | ChatMessage drops the I-prefix ("Hungarian notation") modern TypeScript style avoids; both are exported as aliases, so either name works. |
| Built-in components are exported as ChatBubble, ChatAvatar, ChatComposer, ChatInputToolbar, ChatActions, ChatSystemMessage, MessageRow, MessageList, DateSeparator, MessageTimestamp, LoadEarlierButton, UserAvatar, not Bubble, Avatar, Composer, InputToolbar, Actions, SystemMessage, Message, MessagesContainer, Day, Time, LoadEarlierMessages, GiftedAvatar | Bare names like Avatar, Bubble, Day, Time, Send, Message are easy for a published package to collide with a consumer's own component of the same name. The render* prop names you actually pass to <SoftChat /> — which is the API surface most apps touch — are unaffected; this only matters if you import a built-in component directly (e.g. to wrap it in a custom renderBubble). |
| Send has an icon-based default (with an optional label for the original text button) instead of always rendering text | A modern default look; label/textStyle are still supported for the original text-button appearance. |
If you're porting an existing <GiftedChat /> screen, the mechanical part of the migration is
usually just: change the import to react-native-soft-chat, rename the component to
SoftChat, and — if you imported any of the built-in sub-components directly — add the Chat/
other prefix noted above.
License
MIT. react-native-soft-chat is derived from
react-native-gifted-chat
(Copyright (c) 2019 Farid from Safi), which is also MIT licensed. See LICENSE for
the full text and attribution.
