Simple Chat App With React Native - Part II

In the previous part we discussed the creation of a backend for messaging app. This time I would like to describe how to connect the frontend with our backend. Step by step, I will cover every endpoint by adjusting adequate action for them and finally, present how it works.

This is the second part of Simple chat app with React Native:

What tools do we need?

Once more we need to answer this question. Surely, a tool for the state management is our must have. Why? Conversations and messages are changing constantly, therefore a tool which will help us control the application state is necessary. My first choice was Redux, which manages state through a single JavaScript object, known as a data store, where all of our application’s state is located. We simply have to install redux with the React bindings:

yarn add redux react-redux

Did you hear about Gifted Chat? It’s pretty cool complete chat UI for React Native, which will make displaying messages so much easier. Let’s add it to dependencies:

yarn add react-native-gifted-chat

Connecting to the Socket.IO server

Socket.IO provides bi-directional communication in realtime and works perfectly for chat based applications. What is the biggest advantage of using it? We don’t have to involve polling the server for changes, because server immediately will get a message and push it to connected client.

The Socket.IO server we created recently is listening to the ‘connection’event, which is automatically sent by the client. Our task is to send the user id as an init message, so that the server can identify the user the socket is connected to.

this.socket.emit('init', {
senderId: this.props.user.myId,
});
view raw clientInit.js hosted with ❤ by GitHub

Let’s also add a listener for the ‘message’ event on the server which receives messages from the client and creates a message object to store in the database.

this.socket.on('message', message => {
const newMessage = {
createdAt: message.createdAt,
text: message.text,
userId: message.senderId,
_id: message.msgId,
};
/* sending message function */
});

Need to conduct React Native training in your company? Talk to us!

Actions, reducers and store … so Redux

Firstly, we need to define createConversation action. According to official documentation actions are:

Payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using <rte-code>store.dispatch()<rte-code>.

Let’s dive into code represents creating conversation:

import { NavigationActions } from 'react-navigation';
import callApi from '../helpers/api';
export const createConversationSuccess = conversation => ({
type: 'CREATE_CONVERSATION_SUCCESS',
conversation,
});
export const createConversationFailure = error => ({
type: 'CREATE_CONVERSATION_FAILURE',
error,
});
export const createConversation = friendId => dispatch => {
callApi('/conversations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ friendId }),
})
.then(response => response.json())
.then(conversation => {
if (!conversation.error) {
dispatch(createConversationSuccess(conversation));
dispatch(
NavigationActions.navigate({
routeName: 'ConversationChat',
params: { conversation },
}),
);
} else {
dispatch(createConversationFailure(conversation.message));
}
});
return {
type: 'CREATE_CONVERSATION',
};
};

The action is calling for a specific endpoint, in this case, it’s <rte-code>/conversation<rte-code>. The data that is sent is friendId, so in the conversation are kept two ids: conversation and friend.

Conversation state

Because of <rte-code>redux-thunk<rte-code> middleware, we are able to dispatch action when we get the response. In a case when a response doesn’t include an error, createConversationSuccess() action is dispatched sequentially with navigating to conversation chat view.

Reducer’s job is to specify how the application’s state changes in response. 
When conversation creation is successful, the returned value is an array of conversations, which are spread and overwritten by action payload, so action.conversation. Simultaneously currentConversationId is set on action.conversation.Id.

const initState = {
currentConversationId: '',
conversations: [],
};
export default (state = initState, action) => {
switch (action.type) {
case 'CREATE_CONVERSATION_SUCCESS': {
return {
conversations: [...state.conversations, action.conversation],
currentConversationId: action.conversation.id,
};
}
case 'CREATE_CONVERSATION_FAILURE': {
return initState;
}
default: {
return state;
}
}
};

Creating a message is much simpler. It’s a typical action from the action creator. What does it mean? Just that action creator is a function which returns an action.

export const sendMessage = (conversationId, message) => ({
type: 'SEND_MESSAGE',
conversationId,
message,
});

In our example function is called sendMessage and returns action type ‘SEND_MESSAGE’. The function accepts two arguments, where the first- conversationId is needed to identify a destination for second — message.

We are following optimistic update approach likewise WhatsApp does, which means that a message is immediately appended to conversation before server responds. If it fails, we should display an error and give the user an opportunity to retry his request.

Similarly as before, state is changing in reducer and the result is message added to the exact conversation.

const initState = {};
export default (state = initState, action) => {
switch (action.type) {
case 'SEND_MESSAGE': {
if (state[action.conversationId]) {
return {
...state,
[action.conversationId]: [
...state[action.conversationId],
action.message,
],
};
}
return {
...state,
[action.conversationId]: [action.message],
};
}
default: {
return state;
}
}
};

At this stage, we are ready to load created conversations and sent the messages. The actions for them are very similar. The differences are:

  • an endpoint for which action is dispatched
  • passed argument (conversationId) that is needed only in receiving messages action.
export const loadMessages = conversationId => dispatch => {
callApi(`/messages/${conversationId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(messages => {
if (!messages.error) {
dispatch(loadMessagesSuccess(messages));
} else {
dispatch(loadMessagesFailure(messages.message));
}
})
.catch(err => {
console.log(err);
});
return {
type: 'LOAD_MESSAGES',
};
};
export const sendMessage = (conversationId, message) => ({
type: 'SEND_MESSAGE',
conversationId,
message,
});

How should the state change in reducer? Obviously, it will return state with all messages for given id.

const initState = {};
export default (state = initState, action) => {
console.log(action);
switch (action.type) {
case 'LOAD_MESSAGES_SUCCESS': {
return {
...state,
[action.id]: action.messages,
};
}
case 'LOAD_MESSAGES_FAILURE': {
return initState;
}
default: {
return state;
}
}
};

Store is a perfect place for holding the whole state tree of chat application. It’s worth to split reducing function into separate functions as well and then use combineReducers which is passed to createStore.

It would look like:

const reducers = combineReducers({
user,
friends,
conversations,
messages,
error,
})
const store = createStore(reducers, applyMiddleware(thunk));
view raw createStore.js hosted with ❤ by GitHub

Time for a little chat

Our logic is ready, so let’s create the containers for displaying conversations. Most likely we need to prepare two views:

  • user account panel with friend list with whom we are messaging and option to add mate to chat,
  • chat window for texting a message.

It’s time to tackle the main view called UserAccount, where you have to connect states from reducers and redux action. Here’s how it will look like:

export default connect(
state => ({
user: state.user,
friends: state.friends.friends,
conversations: state.conversations.conversations,
}),
{
onLoadFriends: loadFriends,
onLoadConversations: loadConversations,
onSetCurrentConversationId: setCurrentConversation,
onCreateConversation: createConversation,
},
)(UserAccount);

To receive the user’s friends and all conversations will dispatch two actions inside the lifecycle method componentDidMount(). This approach ensures us that data won’t be loaded until after the initial render and is fetched only from the client.

componentDidMount() {
this.props.onLoadFriends();
this.props.onLoadConversations();
}

So what happens to the other two actions? Let me explain it, but first look at the code below:

<FriendList
data={sortedFriends}
renderItem={({ item }) => (
<FriendItemComponent
fullName={item.fullName}
goToChat={() => {
const conversation = this.findConversation(item._id);
if (conversation && conversation.id) {
this.props.onSetCurrentConversationId(conversation.id);
navigate('ConversationChat', { conversation });
} else {
this.props.onCreateConversation(item._id);
}
}}
/>
)}
keyExtractor={item => {
return item._id;
}}
/>
view raw friendList.js hosted with ❤ by GitHub

FriendList is a styled FlatList which renders a sorted array of friends, where a single friend is an object with _id and fullName. Tapping on friend item calls the onSetCurrentConversationId() function and navigates to chat view when the conversation has been started or otherwise will create new conversation by firing onCreateConversation().

ConversationChat is a view for texting a message, so the most appropriate place to connect sending and loading messages. I think, you are already familiar with it, but just in case check the code snippet:

export default connect(
state => ({
user: state.user,
messages: state.messages[state.conversations.currentConversationId],
friends: state.friends.friends,
}),
{
onLoadMessages: loadMessages,
onSendMessage: sendMessage,
},
)(ConversationChat);

Once again within componentDidMount() we are dispatching action- this time onLoadMessages().

componentDidMount() {
this.props.onLoadMessages(
this.props.navigation.state.params.conversation.id,
);
}

I hope that you are still following, because it’s the time for implementing onSendMessage() action! Let’s start it by creating a function wrapper inside, where we will be sending the ‘message’ event to the server. At the same time, we can update redux state. Then close the function with firing the action.

_onSend = message => {
const { conversation } = this.props.navigation.state.params;
const { user, onSendMessage } = this.props;
this.socket.emit('message', {
conversationId: conversation.id,
text: message[0].text,
senderId: user.myId,
receiverId: conversation.friendId,
createdAt: new Date(),
msgId: message[0]._id,
});
const newMessage = {
createdAt: message[0].createdAt,
text: message[0].text,
userId: message[0].user._id,
_id: message[0]._id,
};
onSendMessage(conversation.id, newMessage);
};

The last step is to use GiftedChat where we pass the following props:

  • messages as an array of messages to display,
  • onSend as a callback when user is sending a message,
  • user, which is just user’s id.
<GiftedChat
messages={this.getMappedMessages()}
onSend={this._onSend}
user={{ _id: this.props.user.myId }}
/>
view raw giftedChat.js hosted with ❤ by GitHub

How does it work?

gif showing final results

I hope you enjoyed this article, and creating simple chat app is easier for you now. Everything you read today can find inside an friendChat app.

Insights

Learn more about Backend

cover

Nov 4Article

Setting Up Notifications With AWS Amplify Gen 1

Learn how to integrate AWS Amplify with React Native for Android, focusing on live messaging and push notifications. This guide walks you through the setup.

cover

Aug 10Podcast

Databases for Mobile Apps: React Native & Realm

Join us on a deep-dive into the complex world of databases for mobile apps, and take a closer look at what the marriage of React Native, Realm & MongoDB can bring.

cover

Oct 23Article

Simple Chat App With React Native - Part I

Learn how to create the backend for a messaging app in React Native with our blog post. We cover the topic in a two-part series.

cover

Oct 3Article

React Native, Firebase and You

Learn how to build a full-stack project in React Native with the Firebase support. Check the benefits of using the react-native-firebase module.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

Writing a Swift TurboModule in React Native

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.