Build low-latency Vision AI applications using our new open-source Vision AI SDK. ⭐️ on GitHub

Tutorial: How to Build a Slack Clone with React Native – Part 1

Using the React Native library and Stream Chat, you can create a collaborative messaging app experience in minutes.

Vishal N.
Vishal N.
Published April 20, 2020 Updated February 24, 2026
Slack clone

In this tutorial, you will create a clone of Slack, a workplace messaging platform in React Native. The Slack application consists of three parts and includes several features.

This first part covers the following UI/UX features:

  • Channel List: Contacts in popular messaging apps like WhatsApp.
  • Input Box: For composing messages.
  • Message Row: For displaying incoming and outgoing messages.
  • Reaction List: To display message reactions.
  • Giphy Card: A container for displaying GIFs.
  • Enriched URL Previews.

The result will look like the following:

Final project

Note: This tutorial is a guide to building a real-world chat experience using Stream's Chat and Messaging API and SDKs—not a production-ready Slack replacement.

Resources

Here are a few links to help you along the tutorial:

Dev Environment and Project Setup

Before getting started, ensure you have a development environment set up for React Native, Android, and iOS. You should read the Set Up Your Environment section of the official React Native docs.

Once you have a dev environment set up, create a new react-native application:

bash
1
2
3
4
5
6
7
8
# Create a new react-native project with the name SlackChatApp npx @react-native-community/cli@latest init SlackChatApp # Go to your app directory cd SlackChatApp # Add all the required dependencies for this project yarn add @react-native-community/netinfo@12.0.1 @react-navigation/native@7.1.28 @react-navigation/drawer@7.8.1 moment@2.30.1 @react-native-documents/picker@12.0.1 react-native-gesture-handler@2.30.0 react-native-image-picker@8.2.1 react-native-reanimated@4.2.1 react-native-safe-area-context@5.6.2 react-native-screens@4.23.0 stream-chat@9.32.0 stream-chat-react-native@8.12.4

Slack uses a Lato font, which is freely available on https://fonts.google.com/. For visual parity, we need to import the font into our app. To do so, create a file named react-native.config.js in the project directory and paste the following contents:

js
1
2
3
module.exports = { assets: ['./src/fonts/'], };

You can download the fonts from the Google Fonts website. You will see a button titled Download family at the top.

Add a Channel List Navigation

The ChannelList chat component helps build the app's primary navigation. You can use it to subscribe to events for channel changes, updates, and new messages. Create a channel list and add it to a navigation drawer ​​in src/components/ChannelList/ChannelList.js.

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import React from 'react'; import {View, StyleSheet, SectionList, TouchableOpacity} from 'react-native'; import {useNavigation, useTheme} from '@react-navigation/native'; import {ChatClientService, notImplemented} from '../../utils'; import {SVGIcon} from '../SVGIcon'; import {SCText} from '../SCText'; import {ChannelListItem} from '../ChannelListItem'; import {useWatchedChannels} from './useWatchedChannels'; export const ChannelList = () => { const client = ChatClientService.getClient(); const navigation = useNavigation(); const {colors} = useTheme(); const changeChannel = channelId => { navigation.navigate('ChannelScreen', {channelId}); }; const { activeChannelId, setActiveChannelId, unreadChannels, readChannels, directMessagingConversations, } = useWatchedChannels(client); const renderChannelRow = (channel, isUnread) => { return ( <ChannelListItem activeChannelId={activeChannelId} setActiveChannelId={setActiveChannelId} changeChannel={changeChannel} showAvatar={false} presenceIndicator isUnread={isUnread} channel={channel} client={client} key={channel.id} currentUserId={client.user.id} /> ); }; return ( <View style={styles.container}> <SectionList showsVerticalScrollIndicator={false} showsHorizontalScrollIndicator={false} style={styles.sectionList} sections={[ { title: '', id: 'menu', data: [ { id: 'threads', title: 'Threads', icon: <SVGIcon height="14" width="14" type="threads" />, handler: notImplemented, }, { id: 'drafts', title: 'Drafts', icon: <SVGIcon height="14" width="14" type="drafts" />, handler: () => navigation.navigate('DraftsScreen'), }, ], }, { title: 'Unread', id: 'unread', data: unreadChannels || [], }, { title: 'Channels', data: readChannels || [], clickHandler: () => { navigation.navigate('ChannelSearchScreen', { channelsOnly: true, }); }, }, { title: 'Direct Messages', data: directMessagingConversations || [], clickHandler: () => { navigation.navigate('NewMessageScreen'); }, }, ]} keyExtractor={(item, index) => item.id + index} SectionSeparatorComponent={() => <View style={{height: 5}} />} renderItem={({item, section}) => { if (section.id === 'menu') { return ( <TouchableOpacity onPress={() => { item.handler && item.handler(); }} style={styles.channelRow}> <View style={styles.channelTitleContainer}> {item.icon} <SCText style={styles.channelTitle}>{item.title}</SCText> </View> </TouchableOpacity> ); } return renderChannelRow(item, section.id === 'unread'); }} stickySectionHeadersEnabled renderSectionHeader={({section: {title, data, id, clickHandler}}) => { if (data.length === 0 || id === 'menu') { return null; } return ( <View style={[ styles.groupTitleContainer, { backgroundColor: colors.background, borderTopColor: colors.border, borderTopWidth: 1, }, ]}> <SCText style={styles.groupTitle}>{title}</SCText> {clickHandler && ( <TouchableOpacity onPress={() => { clickHandler(); }} style={styles.groupTitleRightButton}> <SCText style={styles.groupTitleRightButtonText}>+</SCText> </TouchableOpacity> )} </View> ); }} /> </View> ); }; const styles = StyleSheet.create({ container: { paddingLeft: 5, paddingRight: 5, flexDirection: 'column', justifyContent: 'flex-start', }, sectionList: { flexGrow: 1, flexShrink: 1, }, groupTitleContainer: { paddingTop: 14, marginLeft: 10, marginRight: 10, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, groupTitle: { fontSize: 14, }, groupTitleRightButton: { textAlignVertical: 'center', }, groupTitleRightButtonText: { fontSize: 25, }, channelRow: { paddingLeft: 10, paddingTop: 5, paddingBottom: 5, flexDirection: 'row', justifyContent: 'space-between', borderRadius: 6, marginRight: 5, }, channelTitleContainer: { flexDirection: 'row', alignItems: 'center', }, channelTitle: { padding: 5, paddingLeft: 10, }, });

The app needs to be populated with some channels. In the project’s App.js, replace the content with the following to create a chat client and pass it as a prop to the ChannelList component.

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
import React, {useEffect, useState} from 'react'; import { ActivityIndicator, View, StyleSheet, SafeAreaView, Text, Pressable, LogBox, useColorScheme, } from 'react-native'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {NavigationContainer} from '@react-navigation/native'; import {createStackNavigator} from '@react-navigation/stack'; import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; import {StreamChat} from 'stream-chat'; import {Chat, OverlayProvider} from 'stream-chat-react-native'; import { ChatUserContext, ChatClientService, USER_TOKENS, USERS, } from './src/utils'; import {ChannelScreen} from './src/screens/ChannelScreen'; import {NewMessageScreen} from './src/screens/NewMessageScreen'; import {ChannelSearchScreen} from './src/screens/ChannelSearchScreen'; import {ChannelListScreen} from './src/screens/ChannelListScreen'; import {DraftsScreen} from './src/screens/DraftsScreen'; import {MentionsScreen} from './src/screens/MentionsSearch'; import {DirectMessagesScreen} from './src/screens/DirectMessagesScreen'; import {TargettedMessageChannelScreen} from './src/screens/TargettedMessageChannelScreen'; import {MessageSearchScreen} from './src/screens/MessageSearchScreen'; import {ProfileScreen} from './src/screens/ProfileScreen'; import {ThreadScreen} from './src/screens/ThreadScreen'; import {BottomTabs} from './src/components/BottomTabs'; import {DarkTheme, LightTheme} from './src/appTheme'; LogBox.ignoreAllLogs(true); const Tab = createBottomTabNavigator(); const HomeStack = createStackNavigator(); const ModalStack = createStackNavigator(); const App = () => { const scheme = useColorScheme(); const [connecting, setConnecting] = useState(true); const [connectionError, setConnectionError] = useState(null); const [retryCount, setRetryCount] = useState(0); const [user, setUser] = useState(USERS.vishal); useEffect(() => { let client; let isMounted = true; const initChat = async () => { try { client = StreamChat.getInstance('q95x9hkbyd6p', { timeout: 30000, }); await client.connectUser(user, USER_TOKENS[user.id]); if (isMounted) { ChatClientService.setClient(client); setConnectionError(null); setConnecting(false); } } catch (error) { console.error('Failed to connect to Stream Chat:', error); if (isMounted) { setConnectionError( error?.message || 'Unable to connect. Check your network and retry.', ); setConnecting(false); } } }; setConnecting(true); setConnectionError(null); initChat(); return () => { isMounted = false; if (client) { client.disconnectUser().catch(() => null); } }; }, [user, retryCount]); if (connecting) { return ( <SafeAreaView> <View style={styles.loadingContainer}> <ActivityIndicator size="small" color="black" /> </View> </SafeAreaView> ); } if (connectionError) { return ( <SafeAreaView> <View style={styles.loadingContainer}> <Text style={styles.errorTitle}>Could not connect to chat.</Text> <Text style={styles.errorBody}>{connectionError}</Text> <Pressable style={styles.retryButton} onPress={() => setRetryCount(count => count + 1)}> <Text style={styles.retryButtonText}>Retry</Text> </Pressable> </View> </SafeAreaView> ); } return ( <GestureHandlerRootView style={styles.container}> <SafeAreaProvider> <OverlayProvider> <Chat client={ChatClientService.getClient()}> <NavigationContainer theme={scheme === 'dark' ? DarkTheme : LightTheme}> <ChatUserContext.Provider value={{ switchUser: userId => setUser(USERS[userId]), }}> <HomeStackNavigator /> </ChatUserContext.Provider> </NavigationContainer> </Chat> </OverlayProvider> </SafeAreaProvider> </GestureHandlerRootView> ); }; const ModalStackNavigator = () => { return ( <ModalStack.Navigator initialRouteName="Tabs" screenOptions={{ presentation: 'modal', headerShown: false, }}> <ModalStack.Screen name="Tabs" component={TabNavigation} /> <ModalStack.Screen name="NewMessageScreen" component={NewMessageScreen} /> <ModalStack.Screen name="ChannelSearchScreen" component={ChannelSearchScreen} /> <ModalStack.Screen name="MessageSearchScreen" component={MessageSearchScreen} /> <ModalStack.Screen name="TargettedMessageChannelScreen" component={TargettedMessageChannelScreen} /> </ModalStack.Navigator> ); }; const HomeStackNavigator = () => { return ( <HomeStack.Navigator initialRouteName="ModalStack" screenOptions={{headerShown: false}}> <HomeStack.Screen name="ModalStack" component={ModalStackNavigator} /> <HomeStack.Screen name="ChannelScreen" component={ChannelScreen} /> <HomeStack.Screen name="DraftsScreen" component={DraftsScreen} /> <HomeStack.Screen name="ThreadScreen" component={ThreadScreen} /> </HomeStack.Navigator> ); }; const TabNavigation = () => { return ( <Tab.Navigator tabBar={props => <BottomTabs {...props} />} screenOptions={{headerShown: false}}> <Tab.Screen name="home" component={ChannelListScreen} /> <Tab.Screen name="dms" component={DirectMessagesScreen} /> <Tab.Screen name="mentions" component={MentionsScreen} /> <Tab.Screen name="you" component={ProfileScreen} /> </Tab.Navigator> ); }; export default App; const styles = StyleSheet.create({ loadingContainer: { height: '100%', justifyContent: 'center', alignItems: 'center', paddingHorizontal: 20, }, container: { flex: 1, }, errorTitle: { fontSize: 17, fontWeight: '600', marginBottom: 8, }, errorBody: { textAlign: 'center', marginBottom: 16, }, retryButton: { backgroundColor: '#005FFF', borderRadius: 8, paddingHorizontal: 16, paddingVertical: 10, }, retryButtonText: { color: '#fff', fontWeight: '600', }, });

Next, you should create a hook in ChannelList.js for querying the channels. Check the full implementation in the source code on GitHub. The hook queries the channels using the Stream client. It also sorts them into three categories, which are returned as state variables: unreadChannels, readChannels, and oneOnOneConversations.

Create Channel List Items

Add a new component in a separate file named src/components/ChannelListItem.js. This creates a UI that resembles Slack.

This component will ensure different styles depending on whether it's a group channel, a one-on-one conversation, or an unread channel. It will also check whether or not it contains user mentions.

If you run your app with yarn ios or yarn android, you should see a few channels populated in the channels list, as shown in the screenshot below:

Channel list

So far, the ChannelList works fine, but it's not real-time. If another user sends a message on a channel, it won't appear in your ChannelList. We need to implement event handlers in our useWatchedChannels hook for this purpose.

You can find detailed docs about Stream events here.

We are going to handle two events for tutorial purposes, but you can experiment with as many events as you want:

Get started! Activate your free Stream account today and start prototyping your chat app.

1. message.new - This event tells us that there is a new message on some channel (channel data is included in the event object). In this case, we want to move the channel from either readChannels or oneOnOneConversations to unreadChannels.
2. message.read - This event tells us that some channel (data available in the event object) was marked as read. In this case, we want to move the channel from unreadChannels to either readChannels or oneOnOneConversations.

Let's use the useWatchedChannels hook in src/components/ChannelList/useWatchedChannels.js to support real-time updates:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import {useState, useEffect} from 'react'; import {CacheService, ChatClientService} from '../../utils'; export const useWatchedChannels = () => { const client = ChatClientService.getClient(); const [activeChannelId, setActiveChannelId] = useState(null); const [unreadChannels, setUnreadChannels] = useState([]); const [readChannels, setReadChannels] = useState([]); const [directMessagingConversations, setDirectMessagingConversations] = useState([]); const filters = { type: 'messaging', example: 'slack-demo', members: { $in: [client.user.id], }, }; const sort = {has_unread: -1, last_message_at: -1}; const options = {limit: 30, offset: 0, state: true}; useEffect(() => { const _unreadChannels = []; const _readChannels = []; const _directMessagingConversations = []; const fetchChannels = async () => { const channels = await client.queryChannels( { ...filters, name: {$ne: ''}, }, sort, options, ); channels.forEach(c => { if (c.countUnread() > 0) { _unreadChannels.push(c); } else { _readChannels.push(c); } }); setUnreadChannels([..._unreadChannels]); setReadChannels([..._readChannels]); setDirectMessagingConversations([..._directMessagingConversations]); CacheService.setChannels(channels); }; const fetchDirectMessagingConversations = async () => { const directMessagingChannels = await client.queryChannels( { ...filters, name: '', }, sort, options, ); directMessagingChannels.forEach(c => { if (c.countUnread() > 0) { _unreadChannels.push(c); } else { _directMessagingConversations.push(c); } }); _unreadChannels.sort((a, b) => { return a.state.last_message_at > b.state.last_message_at ? -1 : 1; }); setUnreadChannels([..._unreadChannels]); setReadChannels([..._readChannels]); setDirectMessagingConversations([..._directMessagingConversations]); CacheService.setDirectMessagingConversations(directMessagingChannels); }; async function init() { await fetchChannels(); await fetchDirectMessagingConversations(); CacheService.loadRecentAndOneToOne(); } init(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { function handleEvents(e) { if (e.type === 'message.new') { if (e.user?.id === client.user.id) { return; } const cid = e.cid; const channelReadIndex = readChannels.findIndex( channel => channel.cid === cid, ); if (channelReadIndex >= 0) { const channel = readChannels[channelReadIndex]; readChannels.splice(channelReadIndex, 1); setReadChannels([...readChannels]); setUnreadChannels([channel, ...unreadChannels]); } const dmIndex = directMessagingConversations.findIndex( channel => channel.cid === cid, ); if (dmIndex >= 0) { const channel = directMessagingConversations[dmIndex]; directMessagingConversations.splice(dmIndex, 1); setDirectMessagingConversations([...directMessagingConversations]); setUnreadChannels([channel, ...unreadChannels]); } const channelUnreadIndex = unreadChannels.findIndex( channel => channel.cid === cid, ); if (channelUnreadIndex >= 0) { const channel = unreadChannels[channelUnreadIndex]; unreadChannels.splice(channelUnreadIndex, 1); setUnreadChannels([channel, ...unreadChannels]); } } if (e.type === 'message.read') { if (e.user?.id !== client.user.id) { return; } const cid = e.cid; const channelIndex = unreadChannels.findIndex( channel => channel.cid === cid, ); if (channelIndex < 0) { return; } const channel = unreadChannels[channelIndex]; unreadChannels.splice(channelIndex, 1); setUnreadChannels([...unreadChannels]); if (!channel.data.name) { setDirectMessagingConversations([ channel, ...directMessagingConversations, ]); } else { setReadChannels([channel, ...readChannels]); } } } client.on(handleEvents); return () => { client.off(handleEvents); }; }, [client, readChannels, unreadChannels, directMessagingConversations]); return { activeChannelId, setActiveChannelId, unreadChannels, setUnreadChannels, readChannels, setReadChannels, directMessagingConversations, setDirectMessagingConversations, }; };

We have added another useEffect hook here that adds an event listener to our stream client and removes it when the component unmounts. The handleEvent is an event handler that takes some action based on the event received.

As an exercise, you can try adding handlers for other events such as user.presence.changed, channel.updated, or channel.deleted

Add a Channel Screen

The Channel screen consists of a header, src/components/ChannelHeader.js, MessageInput, and ChannelScreen components.

The MessageInput and ChannelScreen components are provided by Stream as part of the react-native-sdk.

Let’s update the ChannelScreen component in src/screens/ChannelScreen.js.

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import React, {useEffect, useState} from 'react'; import {View, SafeAreaView, StyleSheet} from 'react-native'; import { Channel, MessageList, MessageInput, } from 'stream-chat-react-native'; import {useNavigation, useRoute, useTheme} from '@react-navigation/native'; import {ChannelHeader} from '../components/ChannelHeader'; import {MessageSlack} from '../components/MessageSlack'; import {DateSeparator} from '../components/DateSeparator'; import { getChannelDisplayImage, getChannelDisplayName, ChatClientService, AsyncStore, } from '../utils'; export function ChannelScreen() { const {colors} = useTheme(); const {params} = useRoute(); const channelId = params?.channelId ?? null; const navigation = useNavigation(); const chatClient = ChatClientService.getClient(); const [channel, setChannel] = useState(null); const [isReady, setIsReady] = useState(false); const goBack = () => { navigation.goBack(); }; useEffect(() => { if (!channelId) { navigation.goBack(); return; } const initChannel = async () => { const _channel = chatClient.channel('messaging', channelId); await _channel.watch(); setChannel(_channel); setIsReady(true); }; initChannel(); }, [channelId]); if (!isReady || !channel) { return null; } return ( <SafeAreaView style={{backgroundColor: colors.background}}> <View style={styles.channelScreenContainer}> <ChannelHeader goBack={goBack} channel={channel} /> <View style={[styles.chatContainer, {backgroundColor: colors.background}]}> <Channel channel={channel} keyboardVerticalOffset={80} MessageSimple={MessageSlack} DateSeparator={DateSeparator} forceAlignMessages="left" doSendMessageRequest={async (cid, message) => { AsyncStore.removeItem( `@slack-clone-draft-${chatClient.user.id}-${channelId}`, ); return channel.sendMessage(message); }}> <MessageList onThreadSelect={thread => { navigation.navigate('ThreadScreen', { threadId: thread.id, channelId: channel.id, }); }} /> <MessageInput additionalTextInputProps={{ placeholderTextColor: '#979A9A', placeholder: channel?.data?.name ? 'Message #' + channel.data.name.toLowerCase().replace(' ', '_') : 'Message', }} /> </Channel> </View> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ channelScreenContainer: {flexDirection: 'column', height: '100%'}, chatContainer: { flexGrow: 1, flexShrink: 1, }, });

After this change, you will see messages and an input box at the bottom of our Channel Screen.

To make the UI more like Slack's, here is a list of features from Slack's UI to add.

Slack UI
  • The user name is shown at the top of messages.
  • Avatars (circular user profile pics next to message) should be square.
  • Reactions should be at the bottom of messages.
  • Reaction counts should be shown next to each reaction.
  • URL previews should have a thick left gray border and an offset for their content alignment.
  • All messages should be displayed on the left side of the screen.
  • GIFs are shown differently in Slack channels.
  • The date separator between messages should be shown above a grey line.
  • Send and attach buttons should be below the input box.

The Stream React Native SDK uses MessageSimple as the default message component. But you can also use a custom UI component as a message -- reference here.

To implement all the above features, navigate to the components directory, src/components, and copy the content of the following files into your project.

After adding the above, all you need to do is pass the MessageSlack and DateSeparator to the MessageList component in App.js.

If you refresh the app, you will see the UI now has much better parity with the Slack UI.

Slack-like UI

Add an Input Field

Input Field

The MessageComposer component in the React Native Chat SDK accepts Input as a custom UI component prop to be shown for the input field. You can create the custom component in src/components/InputBox.js.

It handles the following:

  • AutoCompleteInput - To manage input box features such as mentions, sending messages, maintaining enabled/disabled state, etc.
  • SendButton: To send the composed messages.
  • AttachButton: To upload media such as files, documents, images, and videos.

After creating the InputBox, it must be passed to the MessageInput in the ChannelScreen component of App.js.

Congratulations!

Channel and messages screens

You have now completed part one of the three-series Slack clone tutorial using Stream's React Native Chat components.

The next part of the tutorial will cover additional UI components and their functionality, such as:

We’ll see you in part two!

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more