MCPcopy Index your code
hub / github.com/doctolib-lab/react-native-streaming-message-list

github.com/doctolib-lab/react-native-streaming-message-list @v0.2.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.2.0 ↗ · + Follow
47 symbols 133 edges 29 files 0 documented · 0%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

react-native-streaming-message-list

ChatGPT and Claude-style smart scrolling for React Native message lists.

Version bundle size Build Status

demo

A FlatList-compatible React Native component that replicates ChatGPT/Claude-like "new message snaps to top" scrolling behavior for conversational UIs where the last item can grow over time (e.g., streaming AI responses).

Try it live on Expo Snack: AndroidiOSWeb

Features

  • Smart scroll behavior: New messages snap to top with dynamic blank space management
  • Streaming-friendly: Handles growing/updating content without scroll jank
  • FlatList-like API: Familiar props, works with any message data structure

Installation

npm install react-native-streaming-message-list react-native-reanimated

This library requires react-native-reanimated. Follow their installation guide if you haven't already.

Examples

We provide multiple example apps showcasing different use cases:

See the examples README for how to run and switch between demos.

Quick Start

💡 For a complete working example, check out the examples folder.

1. Replace your list component

Replace FlatList with StreamingMessageList. This component is built on @legendapp/list and accepts the same FlatList-like props:

- import { FlatList } from 'react-native';
+ import { StreamingMessageList } from 'react-native-streaming-message-list';

- <FlatList
+ <StreamingMessageList
    data={messages}
    keyExtractor={(item) => item.id}
    renderItem={renderMessage}
  />

2. Add streaming state

Create a state variable to track when messages are actively streaming:

const [isStreaming, setIsStreaming] = useState(false);

Pass it to StreamingMessageList:

  <StreamingMessageList
    data={messages}
    keyExtractor={(item) => item.id}
    renderItem={renderMessage}
+   isStreaming={isStreaming}
  />

3. Wrap your anchor and streaming items

To enable smart scrolling, wrap two special messages:

  • Last user message: Wrap with AnchorItem (this message will stay near the top)
  • Last assistant message: Wrap with StreamingItem (tracks height changes during and after streaming)
import { AnchorItem, StreamingItem } from 'react-native-streaming-message-list';

const renderMessage = ({ item, index }) => {
  const isLastUserMessage =
    item.role === 'user' &&
    messages.findLastIndex(m => m.role === 'user') === index;
  const isLastAssistantMessage =
    item.role === 'assistant' &&
    messages.findLastIndex(m => m.role === 'assistant') === index;

  let content = <YourMessageBubble message={item} />;

  if (isLastUserMessage) {
    content = <AnchorItem>{content}</AnchorItem>;
  } else if (isLastAssistantMessage) {
    content = <StreamingItem>{content}</StreamingItem>;
  }

  return content;
};

That's it! The list will now handle ChatGPT-style scrolling automatically.


Need more? See the examples folder for complete, runnable chat apps with different UI styles.

Common Patterns

When to use each component

  • StreamingMessageList: Your main list component. Use it instead of FlatList for any chat/message list where content can stream or grow.

  • AnchorItem: Wrap the last user message. This keeps it visible near the top while the assistant response grows below it.

  • StreamingItem: Wrap the last assistant message. Keep this wrapper even after streaming ends to track height changes (like action buttons appearing).

Optional animations

For message animations, use Animated.View from react-native-reanimated with the entering prop.

Recommended approach: Animate only the first user message with a fade-in. The "slide-in" effect for new messages happens naturally through the library's placeholder and scroll-to-bottom behavior, so additional slide animations are unnecessary:

import Animated, { FadeIn } from 'react-native-reanimated';

const renderMessage = ({ item, index }) => {
  const isFirstUserMessage = 
    item.role === 'user' && 
    messages.findIndex(m => m.role === 'user') === index;

  let content = <YourMessageBubble message={item} />;

  return (
    <Animated.View entering={isFirstUserMessage ? FadeIn.duration(350) : undefined}>
      {content}
    </Animated.View>
  );
};

See the Reanimated documentation for more animation options.

Typical message flow

  1. User sends a message → mark it as AnchorItem
  2. Assistant starts responding → set isStreaming={true} and wrap the new assistant message with StreamingItem
  3. Assistant finishes → set isStreaming={false}
  4. Repeat for the next turn

Scroll to bottom button

Use useStreamingMessageList to show a button when the user scrolls away from the bottom:

import {
  StreamingMessageList,
  StreamingMessageListProvider,
  useStreamingMessageList,
} from 'react-native-streaming-message-list';

const ScrollToBottomButton = ({ listRef }) => {
  const { isAtEnd, contentFillsViewport } = useStreamingMessageList();

  if (isAtEnd || !contentFillsViewport) return null;

  return (
    <TouchableOpacity
      style={styles.scrollButton}
      onPress={() => listRef.current?.scrollToEnd({ animated: true })}
    >
      <Text>↓</Text>
    </TouchableOpacity>
  );
};

const listRef = useRef(null);

<StreamingMessageListProvider>
  <View style={{ flex: 1 }}>
    <StreamingMessageList ref={listRef} data={messages} ... />
    <ScrollToBottomButton listRef={listRef} />
  </View>
</StreamingMessageListProvider>

API

<StreamingMessageList>

Main component that wraps your message list with smart scroll behavior.

Props

Extends all FlatList props from @legendapp/list, plus:

Prop Type Required Description
data T[] Yes Array of message items
renderItem (info) => ReactNode Yes Function to render each item
keyExtractor (item, index) => string Yes Unique key for each item
isStreaming boolean No Whether content is currently updating (triggers smart scroll)
config StreamingMessageListConfig No Advanced configuration

Config Options

type StreamingMessageListConfig = {
  debounceMs?: number; // Debounce for placeholder height calculations (default: 150)
  placeholderStableDelayMs?: number; // Delay before placeholder is considered stable (default: 200)
  isAtEndThreshold?: number; // Threshold in pixels for isAtEnd calculation (default: 10)
};

<AnchorItem>

Wrapper for the message that should be "anchored" near the top when a new conversation turn begins (typically the last user message).

<AnchorItem>
  <YourMessageBubble />
</AnchorItem>

<StreamingItem>

Wrapper for the last assistant message.

<StreamingItem>
  <YourMessageBubble />
</StreamingItem>

<StreamingMessageListProvider>

Optional provider that enables access to scroll metrics via useStreamingMessageList. Wrap your list and any components that need scroll metrics with this provider.

<StreamingMessageListProvider>
  <StreamingMessageList ... />
  <YourScrollButton />
</StreamingMessageListProvider>

useStreamingMessageList()

Hook to access scroll metrics. Must be used within StreamingMessageListProvider.

import { useStreamingMessageList } from 'react-native-streaming-message-list';

const { isAtEnd, contentFillsViewport } = useStreamingMessageList();
Property Type Description
isAtEnd boolean true when scrolled to bottom (within threshold)
contentFillsViewport boolean true when content height exceeds viewport

How It Works

The component implements ChatGPT-style scrolling by:

  1. Measuring heights: Tracks the "anchor" message (last user message) and "streaming" content (growing assistant response)
  2. Dynamic placeholder: Injects blank space at the bottom so the anchor message lands near the top
  3. Auto-scrolling: Automatically scrolls to show new messages
  4. Debounced updates: Prevents jank during rapid content updates

Contributing

See CONTRIBUTING.md

License

MIT


Made with create-react-native-library

Core symbols most depended-on inside this repo

Shape

Function 47

Languages

TypeScript100%

Modules by API surface

src/hooks/usePlaceholderState.ts8 symbols
src/StreamingMessageList.tsx7 symbols
src/hooks/useScrollBehavior.ts6 symbols
src/scrollCalculations.ts3 symbols
src/StreamingMessageListContext.tsx3 symbols
examples/src/basic/screen.tsx3 symbols
src/StreamingItem.tsx2 symbols
src/AnchorItem.tsx2 symbols
examples/src/shared/ScrollToBottomButton.tsx2 symbols
examples/src/chatgpt/screen.tsx2 symbols
examples/src/chatgpt/components/MessageBubble.tsx2 symbols
examples/src/chatgpt/components/Composer.tsx2 symbols

For agents

$ claude mcp add react-native-streaming-message-list \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page