@sujeetdotkumar/react-native-gifted-chat-performant
v1.0.1
Published
Performant chat UI for React Native — LegendList v3, native Intl, KeyboardChatLegendList
Maintainers
Readme
What's different from the original
This fork replaces the core rendering stack with purpose-built, higher-performance primitives while keeping the full public API intact (except the date/time format props — see Breaking Changes).
List renderer — LegendList v3
The AnimatedFlatList (a react-native-gesture-handler FlatList wrapped in Animated.createAnimatedComponent) has been replaced with KeyboardChatLegendList from @legendapp/list.
LegendList is purpose-built for chat and large lists in React Native. Key advantages over FlatList:
- Separate recycling pools per item type (
'message'vs'day') — prevents layout thrash between different-height items maintainScrollAtEnd,alignItemsAtEnd,initialScrollAtEnd— purpose-built chat scroll semantics without aninvertedprop hacksharedValuesprop syncs scroll offset directly to a ReanimatedSharedValue— nouseAnimatedScrollHandlerneeded- Pure JS, no native code, no extra
pod install
Day separator tracking — eliminated
The original implementation tracked day separator positions using a CellRendererComponent that fired a layout worklet on every message item on every render. Those positions were stored in a daysPositions SharedValue, sorted (O(n log n)), and interpolated on every scroll frame. All of that is gone.
Day separators are now interleaved directly in the data array (displayData) alongside messages. An onViewableItemsChanged callback tracks the topmost visible day for the animated overlay — zero per-item overhead.
Keyboard handling — KeyboardStickyView
KeyboardAvoidingView (which can cause layout jumps on keyboard open) has been replaced with:
KeyboardChatLegendListhandles keyboard-aware scroll internallyKeyboardStickyViewfromreact-native-keyboard-controllerkeeps the input toolbar pinned above the keyboard
dayjs removed — native Intl APIs
dayjs (~5 KB gzipped) has been removed entirely. All date and time formatting now uses the built-in Intl.DateTimeFormat API, which is zero-cost (already part of the JS engine), supports the same locale strings, and produces identical output.
lodash.isequal removed
lodash.isequal was listed as a dependency in the original but was not imported anywhere in src/. Removed.
React.memo on leaf components
Day, Time, and GiftedAvatar are now wrapped with React.memo to prevent unnecessary re-renders when parent state changes don't affect their props.
Performance summary
| Bottleneck | Original | This fork |
|---|---|---|
| List virtualization | RNGH FlatList + Animated wrapper | LegendList v3 with separate recycling pools |
| Day position tracking | O(n log n) sort + worklet per item layout | Eliminated — data-driven via displayData |
| Per-item CellRendererComponent | Fires worklet on every item layout | Removed |
| daysPositions SharedValue | Modified per-item, read per scroll frame | Removed |
| dayjs bundle | ~5 KB gzipped | 0 (native Intl) |
| lodash.isequal | ~4 KB gzipped | 0 (removed) |
Breaking Changes
Date & time format props
The original dayjs-based format props have been replaced with Intl.DateTimeFormatOptions:
| Original prop | Type | Replacement | Type |
|---|---|---|---|
| timeFormat | string (dayjs format, e.g. 'LT') | timeFormatOptions | Intl.DateTimeFormatOptions |
| dateFormat | string (dayjs format, e.g. 'D MMMM') | dateFormatOptions | Intl.DateTimeFormatOptions |
| dateFormatCalendar | object (dayjs calendar options) | removed — use dateFormatOptions | — |
Migration example:
// Before
<GiftedChat timeFormat='HH:mm' dateFormat='D MMMM' />
// After
<GiftedChat
timeFormatOptions={{ hour: '2-digit', minute: '2-digit', hour12: false }}
dateFormatOptions={{ day: 'numeric', month: 'long' }}
/>The locale prop continues to work — it is passed directly to Intl.DateTimeFormat as the locale string.
Features
- Fully customizable — override any component with your own implementation
- Composer actions — attach photos, files, or trigger custom actions
- Reply to messages — swipe-to-reply with reply preview and message threading
- Load earlier messages — infinite scroll with pagination support
- Copy to clipboard — long-press messages to copy text
- Smart link parsing — auto-detect URLs, emails, phone numbers, hashtags, mentions
- Avatars — user initials or custom avatar images
- Localized dates — full i18n support via native
Intl.DateTimeFormat - Keyboard handling —
KeyboardChatLegendList+KeyboardStickyViewfor all platforms - System messages — display system notifications in chat
- Quick replies — bot-style quick reply buttons
- Typing indicator — show when users are typing
- Message status — tick indicators for sent/delivered/read states
- Scroll to bottom — quick navigation button
- Web support — works with react-native-web
- Expo support — easy integration with Expo projects
- TypeScript — complete TypeScript definitions included
Requirements
| Requirement | Version | |-------------|---------| | React Native | >= 0.70.0 | | iOS | >= 13.4 | | Android | API 21+ (Android 5.0) | | Expo | SDK 50+ | | react-native-keyboard-controller | >= 1.21.0 | | TypeScript | >= 5.0 (optional) |
Installation
Expo Projects
npx expo install @sujeetdotkumar/react-native-gifted-chat-performant @legendapp/list react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-keyboard-controllerBare React Native Projects
yarn add @sujeetdotkumar/react-native-gifted-chat-performant @legendapp/list react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-keyboard-controllerThen install iOS pods:
npx pod-installAnd follow the react-native-reanimated installation guide to add the Babel plugin.
Usage
import React, { useState, useCallback, useEffect } from 'react'
import { GiftedChat } from '@sujeetdotkumar/react-native-gifted-chat-performant'
import { useHeaderHeight } from '@react-navigation/elements'
export function Example() {
const [messages, setMessages] = useState([])
const headerHeight = useHeaderHeight()
useEffect(() => {
setMessages([
{
_id: 1,
text: 'Hello developer',
createdAt: new Date(),
user: {
_id: 2,
name: 'John Doe',
avatar: 'https://placeimg.com/140/140/any',
},
},
])
}, [])
const onSend = useCallback((messages = []) => {
setMessages(previousMessages =>
GiftedChat.append(previousMessages, messages),
)
}, [])
return (
<GiftedChat
messages={messages}
onSend={messages => onSend(messages)}
user={{ _id: 1 }}
keyboardAvoidingViewProps={{ keyboardVerticalOffset: headerHeight }}
/>
)
}Props Reference
Core Configuration
messages(Array) - Messages to displayuser(Object) - User sending the messages:{ _id, name, avatar }onSend(Function) - Callback when sending a messagemessageIdGenerator(Function) - Generate an id for new messages. Defaults to a simple random string generator.locale(String) - Locale string passed toIntl.DateTimeFormat(e.g.'fr','de','ja')colorScheme('light' | 'dark') - Force color scheme. Whenundefined, uses the system color scheme.
Refs
messagesContainerRef(LegendList ref) - Ref to the listtextInputRef(TextInput ref) - Ref to the text input
Keyboard & Layout
keyboardProviderProps(Object) - Props forKeyboardProviderkeyboardAvoidingViewProps(Object) - Props includingkeyboardVerticalOffset(distance from screen top to GiftedChat container — useuseHeaderHeight()when inside a navigation stack)isAlignedTop(Boolean) - Align bubbles to top instead of bottom; defaultfalseisInverted(Bool) - Reverses display order ofmessages; defaulttrue
Text Input & Composer
text(String) - Controlled input textinitialText(String) - Initial text in the input fieldisSendButtonAlwaysVisible(Bool) - Always show send button; defaultfalseminComposerHeight/maxComposerHeight(Number) - Composer height boundsminInputToolbarHeight(Integer) - Minimum toolbar height; default44renderInputToolbar(Component | Function) - Custom input toolbarrenderComposer(Component | Function) - Custom text inputrenderSend(Component | Function) - Custom send buttonrenderActions(Component | Function) - Custom action button (left of composer)renderAccessory(Component | Function) - Custom second line below composertextInputProps(Object) - Props passed to<TextInput>
Messages & Message Container
messagesContainerStyle(Object) - Custom style for messages containerrenderMessage(Component | Function) - Custom message containerrenderLoading(Component | Function) - Loading view while initializingrenderChatEmpty(Component | Function) - Component when messages are emptyrenderChatFooter(Component | Function) - Component below the message listlistProps(Object) - Extra props passed to the underlyingLegendList
Message Bubbles & Content
renderBubble(Component | Function) - Custom message bubblerenderMessageText(Component | Function) - Custom message textrenderMessageImage(Component | Function) - Custom message imagerenderMessageVideo(Component | Function) - Custom message videorenderMessageAudio(Component | Function) - Custom message audiorenderCustomView(Component | Function) - Custom view inside the bubbleisCustomViewBottom(Bool) - Render custom view below text/image; defaultfalseonPressMessage/onLongPressMessage(Function) - Message tap/long-press callbacksimageProps/imageStyle/videoProps- Image and video customizationmessageTextProps(Object) - Props forMessageText(link parsing, matchers, styles)
Avatars
renderAvatar(Component | Function | null) - Custom avatar;nullto hideisUserAvatarVisible(Bool) - Show avatar for current user; defaultfalseisAvatarVisibleForEveryMessage(Bool) - Show avatar on every message; defaultfalseonPressAvatar/onLongPressAvatar(Function) - Avatar tap callbacksisAvatarOnTop(Bool) - Show avatar at top of consecutive messages; defaultfalse
Username
isUsernameVisible(Bool) - Show username in bubble; defaultfalserenderUsername(Component | Function) - Custom username component
Date & Time
timeFormatOptions(Intl.DateTimeFormatOptions) - Format for message times; default{ hour: '2-digit', minute: '2-digit' }dateFormatOptions(Intl.DateTimeFormatOptions) - Format for day separators; default{ day: 'numeric', month: 'long' }renderDay(Component | Function) - Custom day separatorrenderTime(Component | Function) - Custom time componenttimeTextStyle(Object) - Custom time text style (supports left/right)isDayAnimationEnabled(Bool) - Animated day label on scroll; defaulttrue
System Messages
renderSystemMessage(Component | Function) - Custom system message
Load Earlier Messages
loadEarlierMessagesProps(Object)isAvailable- Show/hide buttononPress- Load callbackisLoading- Show spinnerisInfiniteScrollEnabled- Auto-triggeronPressat top of listlabel,containerStyle,wrapperStyle,textStyle- Customization
renderLoadEarlier(Component | Function) - Custom load-earlier button
Typing Indicator
isTyping(Bool) - Show typing indicator; defaultfalserenderTypingIndicator(Component | Function) - Custom typing indicatorrenderFooter(Component | Function) - Custom footer (overrides typing indicator)
Quick Replies
onQuickReply(Function) - Callback when quick reply is sentrenderQuickReplies/renderQuickReplySend(Function) - Custom renderersquickReplyStyle/quickReplyTextStyle/quickReplyContainerStyle- Styles
Reply to Messages
<GiftedChat
reply={{
swipe: {
isEnabled: true,
direction: 'left',
onSwipe: (message) => setReplyMessage(message),
},
message: replyMessage,
onClear: () => setReplyMessage(null),
}}
/>Full reply prop shape:
interface ReplyProps<TMessage> {
swipe?: {
isEnabled?: boolean
direction?: 'left' | 'right'
onSwipe?: (message: TMessage) => void
renderAction?: (progress, translation, position) => React.ReactNode
actionContainerStyle?: StyleProp<ViewStyle>
}
previewStyle?: { containerStyle?, textStyle?, imageStyle? }
messageStyle?: { containerStyle?, textStyle?, imageStyle?, ...left/right variants }
message?: ReplyMessage
onClear?: () => void
onPress?: (message: TMessage) => void
renderPreview?: (props: ReplyPreviewProps) => React.ReactNode
renderMessageReply?: (props: MessageReplyProps) => React.ReactNode
}Scroll to Bottom
isScrollToBottomEnabled(Bool) - Show scroll-to-bottom button; defaultfalsescrollToBottomComponent(Function) - Custom button contentscrollToBottomStyle/scrollToBottomContentStyle- Styles
Platform Notes
Android
Add android:windowSoftInputMode="adjustResize" to your AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize">Web (react-native-web)
@legendapp/list is pure JS and works with react-native-web. Follow the standard react-native-web webpack config to alias react-native imports.
Contributing
yarn install
yarn build # outputs to lib/
yarn test # runs all tests
yarn lint # check for lint errors
yarn lint:fix # auto-fix lint errors
yarn prepublishOnly # full validation: lint + test + buildCredits
Based on react-native-gifted-chat by Farid Safi and contributors.
Performance improvements by Sujeet Kumar.
