DEV Community

Cover image for How to Build Chat Functionality into Your App with React Native [Part 2]
Upsilon
Upsilon

Posted on • Updated on

How to Build Chat Functionality into Your App with React Native [Part 2]

In the second part of our article ‘How to Build Chat Functionality into Your App with React Native,’ we will cover key steps of developing a chat application that is based on Sendbird Chat API and SDK.


Before building a simple React Native chat app with Sendbird Chat SDK, let’s outline its functionality. For our piece of software, we will develop the following features and functions:

  • ‘Signup/Login’ feature — allows a user to access the application.
  • ‘Enter the channel’ and ‘Channels List’ feature — allows a user to enter a channel to send and receive messages and handle the list of messaging channels.
  • ‘Chat’ feature — allows a user to input characters, send and receive messages in real-time.

Now, let’s dive deeper into Sendbird’s Chat SDK - a solution through which you can efficiently integrate real-time chat into your client app.

With the help of Sendbird, you can initialize, configure and build a chat with minimal effort. On the server side, Sendbird ensures reliable infra-management services for your chat within the app.

Sendbird features two fundamental APIs: Client APIs (including the React Native one) and a Platform API. The Platform API, which will not be covered in this article, allows you to communicate with the Sendbird back-end from your back-end or server application (server-to-server communication), which is extremely handy if you want to trigger actions from your Rails, Meteor, or Node app based on an event (for example, push notifications is a typical scenario). In this study, we will focus on building a simple chat app with Sendbird JavaScript SDK.

Sendbird Chat SDK Installation and Configuration

Step 1: Creating a new Sendbird Application

The creation of a new application in the Sendbird console is straightforward. First, we login into our Sendbird account, then go to the dashboard and click Create + at the top-right corner. After that, unique app credentials are generated:

Sendbird dashboard

Using these credentials, we can go further to the Chat SDK installation.

Step 2: Installing the Sendbird Chat SDK

If you’re familiar with using external libraries or SDKs, installing the Chat SDK is simple. You can install the Chat SDK with package manager npm or yarn by entering the command below on the command line.

For NPM (Note: to use npm to install the Chat SDK, Node.js must be first installed on your system): $ npm install sendbird (request to npm server)

For Yarn: $ yarn add sendbird

Step 3: Initializing the Sendbird Chat SDK

For using the features of the Chat SDK, a Sendbird instance must be initiated in our app before user authentication with the Sendbird server.

To initialize a Sendbird instance, pass the App_ID of your Sendbird application in the dashboard as an argument to a parameter in the new Sendbird() method. The new SendBird() must be called once across your client app. Typically, initialization is implemented in the user login view.

var sb = new SendBird({appId: APP_ID});

Step 4: Connecting to the Sendbird Server

After initialization using a new SendBird(), our app must always be connected to the Sendbird server before calling any methods. There are two methods of connecting a user to the Sendbird server: through a unique user ID or in combination with an access token.

Connecting a user to Sendbird server using their unique user ID:

sb.connect(USER_ID, function(user, error) {
if (error) {
// Handle error.
}

Using Chat Platform API, you can create a user along with their own access token or issue an access token for an existing user. Once an access token is issued, a user must provide the access token in the sb.connect() method used for logging in.

Connecting a user to Sendbird server using a combination of a user ID and access token ID:

sb.connect(USER_ID, ACCESS_TOKEN, function(user, error) {
if (error) {
// Handle error.
}

After installing and configuring the Sendbird Chat SDK and connecting our app to the Sendbird server, we can start working with basic chat functionality.

Building Chat App Functionality with Sendbird

‘Signup/Login’ feature development

Let’s discover the signup/login feature that will allow a user to access the application. In our project, we used authentication through a unique user ID.

For authentication, we send a request for connection with a user’s data to the server. Then the server queries the database to check for a match upon the connection request. If no matching user ID is found, the server creates a new user account with the user ID. If an existing user ID is found, it is allowed to log in directly. So, the Sendbird SDK ‘remembers’ the user ID, and for further requests, there is no need to log in until the user logs out from his account. Have a look at how it’s implemented in our sample chat app:

import React, { useState } from 'react';
import { View, StyleSheet } from 'react-native';
import { Input, Button } from 'react-native-elements';
import SendBird from 'sendbird';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { LoginSendBirdProps } from '../../navigation/types';
import { SENDBIRD_API, USER_DATA } from '../consts';
import { useDispatch } from 'react-redux';
import { userLogin } from '../../store/actions';

const LoginSendBird = ({ navigation }: LoginSendBirdProps) => {
  const [userId, setUserId] = useState('');
  const [nickname, setNickname] = useState('');
  const [error, setError] = useState({
    code: 0,
    message: '',
  });
  const dispatch = useDispatch();

  const onButtonPress = () => {
    const sb = new SendBird({ appId: SENDBIRD_API });
    sb.connect(userId, (user, err) => {
      if (err) {
        setError(err);
      } else {
        sb.updateCurrentUserInfo(nickname, '', async (user, err) => {
          if (err) {
            setError(err);
          } else {
            dispatch(userLogin({ nickname, userId }));
            await AsyncStorage.setItem(
              USER_DATA,
              JSON.stringify({ userId, nickname })
            );
            setUserId('');
            setNickname('');
            setError({ code: 0, message: '' });
          }
        });
      }
    });
  };

  return (
    <View style={styles.container}>
      <View style={styles.box}>
        <Input
          label="User ID"
          value={userId}
          labelStyle={{ color: '#000' }}
          onChangeText={setUserId}
          errorMessage={error.message}
          autoCapitalize="none"
        />
      </View>
      <View style={styles.box}>
        <Input
          label="Nickname"
          value={nickname}
          labelStyle={styles.input}
          onChangeText={setNickname}
          errorMessage={error.message}
          autoCapitalize="none"
        />
      </View>
      <View style={styles.box}>
        <Button
          buttonStyle={styles.button}
          title="Connect"
          onPress={onButtonPress}
        />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#fff',
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 20,
  },
  box: {
    marginTop: 10,
  },
  button: { backgroundColor: '#2096f3' },
  input: { color: '#000' },
});

export default LoginSendBird;
Enter fullscreen mode Exit fullscreen mode

Note that the ID should be unique within a Sendbird application to be distinguished from others, such as a hashed email address or phone number in your service.

‘Enter the channel’ and ‘Channels List’ feature development

Sendbird's SDKs and API provide two basic types of channels:

  • Open channels — a Twitch-style public chat. Anyone may join and participate in the conversation without permission.
  • Group channels — a chat that allows close interactions among a limited number of users. A public group channel can be joined by any user without an invitation. Users can freely enter the channel if they want to. On the other hand, a private group channel can let a user join the chat through an invitation by another user who is already a member of the chat room.

To start receiving and sending messages, a user must enter an open channel. The user can enter up to 10 open channels at once, which are valid only within a current connection.

A group channel can consist of 1 to 100 members by default setting. This default number of members can increase per request. A user can view and handle a list of channels that they are a member of.

Here is how this feature works in our sample chat app:

import React, { useState, useCallback, useEffect } from 'react';
import { FlatList, View, RefreshControl, StyleSheet } from 'react-native';
import { ListItem, Avatar, Button } from 'react-native-elements';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useDispatch } from 'react-redux';
import axios from 'axios';
import SendBird from 'sendbird';
import { MASTER_API_TOKEN, SENDBIRD_API, USER_DATA } from '../consts';
import { MenuSendBirdProps } from '../../navigation/types';
import { userLogout } from '../../store/actions';

type SendBirdUser = {
  created_at: number;
  discovery_keys: [];
  has_ever_logged_in: boolean;
  is_active: boolean;
  is_hide_me_from_friends: boolean;
  is_online: boolean;
  last_seen_at: number;
  metadata: Object;
  nickname: string;
  phone_number: string;
  preferred_languages: [];
  profile_url: string;
  require_auth_for_profile_image: boolean;
  user_id: string;
};

const MenuScene = (props: MenuSendBirdProps) => {
  const [users, setUsers] = useState([]);
  const [isRefreshing, setRefreshing] = useState(false);
  const dispatch = useDispatch();

  const getData = useCallback(async () => {
    try {
      setRefreshing(true);
      const response = await axios.get(
        `https://api-${SENDBIRD_API}.sendbird.com/v3/users`,
        {
          headers: {
            'Content-Type': 'application/json; charset=utf8',
            'Api-Token': MASTER_API_TOKEN,
          },
        }
      );
      setUsers(response.data.users);
      setRefreshing(false);
    } catch (error) {
      setRefreshing(false);
      console.error(error);
    }
  }, [users]);

  useEffect(() => {
    props.navigation.setOptions({
      title: 'Chat List',
      headerStyle: {
        backgroundColor: '#a5a8b3',
      },
      headerTintColor: '#454851',
      headerTitleStyle: {
        fontWeight: 'bold',
      },
    });
    getData();
  }, []);

  const logout = async () => {
    await AsyncStorage.removeItem(USER_DATA);
    dispatch(userLogout());
    // console.log(await AsyncStorage.getItem(USER_DATA));
    // console.log(store.getState());
  };

  const startChat = (receiverId: string) => {
    const sb = SendBird.getInstance();

    sb.GroupChannel.createChannelWithUserIds(
      [receiverId],
      true,
      (createdChannel, error) => {
        if (error) {
          console.error(error);
        } else {
          props.navigation.navigate('ChatSendBird', {
            channel: createdChannel,
          });
        }
      }
    );
  };

  const renderItem = ({ item }: { item: SendBirdUser }) => (
    <ListItem
      key={item.user_id}
      bottomDivider
      onPress={() => startChat(item.user_id)}
    >
      <Avatar rounded source={require('../images/user.png')} />
      <ListItem.Content>
        <ListItem.Title>
          {item.nickname}
          <ListItem.Title style={styles.listItemTitle}>
            #{item.user_id}
          </ListItem.Title>
        </ListItem.Title>
        <ListItem.Subtitle>
          {item.is_online ? (
            <ListItem.Title style={styles.listItemTitleOnline}>
              Online
            </ListItem.Title>
          ) : (
            <ListItem.Title style={styles.listItemTitleOffline}>
              Offline
            </ListItem.Title>
          )}
        </ListItem.Subtitle>
      </ListItem.Content>
    </ListItem>
  );

  return (
    <View style={styles.container}>
      <FlatList
        style={styles.flatListStyle}
        contentContainerStyle={styles.flatListContentStyle}
        data={users}
        renderItem={renderItem}
        refreshControl={
          <RefreshControl
            refreshing={isRefreshing}
            onRefresh={() => getData()}
          />
        }
        keyExtractor={(item, index) => `${item.created_at} ${item.nickname}`}
      />
      <Button
        buttonStyle={styles.button}
        title="Logout from SendBird"
        onPress={logout}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#fff', paddingHorizontal: 20 },
  flatListStyle: { flex: 1 },
  flatListContentStyle: { flexGrow: 1 },
  listItemContent: { flexDirection: 'row', justifyContent: 'space-between' },
  listItemTitle: { color: '#0000f0', fontSize: 14 },
  listItemTitleOnline: { color: 'green', fontSize: 14 },
  listItemTitleOffline: { color: 'red', fontSize: 14 },
  button: {
    marginBottom: 40,
  },
});

export default MenuScene;
Enter fullscreen mode Exit fullscreen mode

‘Chat’ feature

Finally, let’s send our first message. The ‘Chat’ feature allows users to send and receive messages in real-time in entered chat rooms. There are three types: a user message, which is a plain text; a file message, which is a binary file (for example, an image or PDF); and an admin message, which is a plain text sent through the dashboard or Chat Platform API.

As a first step, we need to create a chat UI component. For this purpose, we used react-native-gifted-chat, an extensible open-source library with plenty of customizable components that saves tons of time and development effort when building chat UIs.

Then, we can start messaging in real-time in entered chat rooms. Have a look at how it’s done in our sample chat app:

import React, { useState, useEffect, useReducer } from 'react';
import { useSelector } from 'react-redux';
import { GiftedChat } from 'react-native-gifted-chat';
import SendBird from 'sendbird';
import { ChatSendBirdProps } from '../../navigation/types';
import { chatReducer } from '../../store/chatReducer';
import { withAppContext } from '../../store/context';
import { AppState } from 'react-native';

const ChatScene = (props: ChatSendBirdProps) => {
  const { navigation, route } = props;
  const { channel } = route.params;

  const sendbird = SendBird.getInstance();

  const user = useSelector((state: any) => state.user);

  const [query, setQuery] = useState(null);

  const [state, dispatch] = useReducer(chatReducer, {
    channel,
    messages: [],
    messageMap: {}, // redId => boolean
    loading: false,
    input: '',
    empty: '',
    error: '',
  });

  // on state change
  useEffect(() => {
    props.navigation.setOptions({
      // @ts-ignore
      title: channel.members[0].nickname,
      headerStyle: {
        backgroundColor: '#a5a8b3',
      },
      headerTintColor: '#454851',
      headerTitleStyle: {
        fontWeight: 'bold',
      },
    });
    sendbird.addConnectionHandler('chat', connectionHandler);
    sendbird.addChannelHandler('chat', channelHandler);
    AppState.addEventListener('change', handleStateChange);
    if (!sendbird.currentUser) {
      sendbird.connect(user.userId, (err, _) => {
        if (!err) {
          refresh();
          console.log('refreshed');
        } else {
          console.log('refresh error ', err);
          dispatch({
            type: 'error',
            payload: {
              error: 'Connection failed. Please check the network status.',
            },
          });
        }
      });
    } else {
      refresh();
    }

    return () => {
      sendbird.removeConnectionHandler('chat');
      sendbird.removeChannelHandler('chat');
      AppState.removeEventListener('change', handleStateChange);
    };
  }, []);

  /// on query refresh
  useEffect(() => {
    if (query) next();
  }, [query]);

  /// on connection event
  const connectionHandler = new sendbird.ConnectionHandler();
  connectionHandler.onReconnectStarted = () => {
    dispatch({
      type: 'error',
      payload: {
        error: 'Connecting..',
      },
    });
  };
  connectionHandler.onReconnectSucceeded = () => {
    dispatch({
      type: 'error',
      payload: {
        error: '',
      },
    });
    refresh();
  };
  connectionHandler.onReconnectFailed = () => {
    dispatch({
      type: 'error',
      payload: {
        error: 'Connection failed. Please check the network status.',
      },
    });
  };

  /// on channel event
  const channelHandler = new sendbird.ChannelHandler();
  channelHandler.onMessageReceived = (targetChannel, message) => {
    if (targetChannel.url === channel.url) {
      dispatch({ type: 'receive-message', payload: { message: message } });
    }
  };
  channelHandler.onMessageUpdated = (targetChannel, message) => {
    if (targetChannel.url === channel.url) {
      dispatch({ type: 'update-message', payload: { message } });
    }
  };
  channelHandler.onMessageDeleted = (targetChannel, messageId) => {
    if (targetChannel.url === channel.url) {
      dispatch({ type: 'delete-message', payload: { messageId } });
    }
  };

  const handleStateChange = (newState) => {
    if (newState === 'active') {
      sendbird.setForegroundState();
    } else {
      sendbird.setBackgroundState();
    }
  };

  const refresh = () => {
    channel.markAsRead();
    setQuery(channel.createPreviousMessageListQuery());
    dispatch({ type: 'refresh' });
    console.log('refreshed');
  };

  const next = () => {
    if (query.hasMore) {
      dispatch({ type: 'error', payload: { error: '' } });
      query.limit = 50;
      query.reverse = true;
      query.load((fetchedMessages, err) => {
        if (!err) {
          console.log('loadmore');
          dispatch({
            type: 'fetch-messages',
            payload: { messages: fetchedMessages },
          });
        } else {
          console.log('fetch error', err);
          dispatch({
            type: 'error',
            payload: { error: 'Failed to get the messages.' },
          });
        }
      });
    }
  };

  const sendUserMessage = () => {
    if (state.input.length > 0) {
      const params = new sendbird.UserMessageParams();
      params.message = state.input;

      const pendingMessage = channel.sendUserMessage(params, (message, err) => {
        if (!err) {
          dispatch({ type: 'send-message', payload: { message } });
        } else {
          setTimeout(() => {
            dispatch({
              type: 'error',
              payload: { error: 'Failed to send a message.' },
            });
            dispatch({
              type: 'delete-message',
              payload: { reqId: pendingMessage.reqId },
            });
          }, 500);
        }
      });
      dispatch({
        type: 'send-message',
        payload: { message: pendingMessage, clearInput: true },
      });
    }
  };

  return (
    <GiftedChat
      messages={state.messages}
      onSend={sendUserMessage}
      text={state.input}
      renderUsernameOnMessage
      onInputTextChanged={(content) => {
        if (content.length > 0) {
          channel.startTyping();
        } else {
          channel.endTyping();
        }
        dispatch({ type: 'typing', payload: { input: content } });
      }}
      user={{ _id: user.userId, name: user.nickname }}
    />
  );
};

export default withAppContext(ChatScene);
Enter fullscreen mode Exit fullscreen mode

For storing and processing messages and managing shared data across many screens, we used the Redux library. Here are several lines of code of our reducer (Redux state holder):

const initialState = {
}

export const chatReducer = (state = initialState, action) => {
    switch (action.type) {
        case 'refresh': {
            return {
                ...state,
                messageMap: {},
                messages: [],
                loading: false,
                error: null
            };
        }
        case 'fetch-messages': {
            const { messages } = action.payload || {};

            const distinctMessages1 = messages.filter(message => !state.messageMap[message.reqId]);
            const distinctMessages = distinctMessages1.map((chatMessage) => {
                let gcm = {
                    _id: chatMessage.messageId,
                    text: chatMessage.message,
                    createdAt: chatMessage.createdAt,
                    user: {
                        _id: chatMessage?._sender?.userId,
                        name: `${chatMessage._sender?.nickname}#${chatMessage._sender?.userId}`,
                    }
                };
                return gcm;
            });
            const mergedMessages = [...state.messages, ...distinctMessages];
            for (let i = 0; i < mergedMessages.length - 1; i++) {
                mergedMessages[i].hasSameSenderAbove =
                    mergedMessages[i]._sender &&
                    mergedMessages[i + 1]._sender &&
                    mergedMessages[i]._sender.userId === mergedMessages[i + 1]._sender.userId;
            }

            const messageMap = {};
            for (let i in distinctMessages) {
                const message = distinctMessages[i];
                messageMap[message.reqId] = true;
            }
            return {
                ...state,
                messages: mergedMessages,
                messageMap,
                empty: mergedMessages.length === 0 ? 'Start conversation.' : ''
            };
        }
        case 'send-message':
        case 'receive-message':
        case 'update-message': {
            const { message, clearInput } = action.payload || {};
            if (!state.messageMap[message.reqId]) {
                if (state.messages.length > 0) {
                    message.hasSameSenderAbove =
                        message._sender && state.messages[0]._sender && message._sender.userId === state.messages[0]._sender.userId;
                }
                return {
                    ...state,
                    messages: [message, ...state.messages],
                    messageMap: { ...state.messageMap, [message.reqId]: true },
                    input: clearInput ? '' : state.input,
                    empty: ''
                };
            } else {
                for (let i in state.messages) {
                    if (state.messages[i].reqId === message.reqId) {
                        const updatedMessages = [...state.messages];
                        message.hasSameSenderAbove = updatedMessages[i].hasSameSenderAbove;
                        const messageCopy = {
                            _id: message.messageId,
                            text: message.message,
                            createdAt: message.createdAt,
                            user: {
                                _id: message?._sender?.userId,
                                name: `${message._sender?.nickname}#${message._sender?.userId}`,
                            }
                        };
                        updatedMessages[i] = messageCopy;
                        return {
                            ...state,
                            input: clearInput ? '' : state.input,
                            messages: updatedMessages
                        };
                    }
                }
            }
            break;
        }
        case 'delete-message': {
            const { messageId, reqId } = action.payload || {};
            for (let i in state.messages) {
                if (state.messages[i].messageId === messageId || state.messages[i].reqId === reqId) {
                    const updatedMessages = state.messages.filter(m => m.reqId !== reqId && m.messageId !== messageId);
                    for (let i = 0; i < updatedMessages.length - 1; i++) {
                        updatedMessages[i].hasSameSenderAbove =
                            updatedMessages[i]._sender &&
                            updatedMessages[i + 1]._sender &&
                            updatedMessages[i]._sender.userId === updatedMessages[i + 1]._sender.userId;
                    }
                    return {
                        ...state,
                        messages: updatedMessages
                    };
                }
            }
            break;
        }
        case 'typing': {
            const { input } = action.payload || {};
            return { ...state, input };
        }
        case 'start-loading': {
            return { ...state, loading: true, error: '' };
        }
        case 'end-loading': {
            const { error } = action.payload || {};
            return { ...state, loading: false, error };
        }
        case 'error': {
            const { error } = action.payload || {};
            return { ...state, error };
        }
    }
    return state;
};
Enter fullscreen mode Exit fullscreen mode

Twilio vs. Sendbird: Key Features Comparison

Till now, we have gone through all the main steps of building a sample React Native chat app using two real-time chat and messaging platforms - Twilio and Sendbird. Both of them allow configuring, building, and then integrating chat functionality into any client web or mobile app with minimum effort. But to clarify what solution can suit your business needs better, let’s compare Twilio and Sendbird by key features - for better exposure.

Twilio vs. Sendbird comparison

So what's the best alternative for building and implementing chat functionality?

Twilio is the leading cloud communication platform working on Platform-as-a-service (PaaS) model principles. Twilio provides a software-based platform, and still, it's not into in-app chat applications as others. It is also considered a developers' platform; therefore, it cannot be used by marketers or non-developers. The provider mainly targets programmable chat API, allowing users to make and receive phone calls and send and receive text messages instantly.

Sendbird is a chat API and messaging SDK platform with SaaS (Software-as-a-service) model. Since it is a SaaS model, its customers can simply use the application with the available features without making any changes. They don't have to buy, install, maintain, or update any software to use Sendbird.

What's the verdict? The answer may seem trivial, but the dilemma of choosing between Twilio and Sendbird lies in your business goals, the tech skills available, and the chat features your client application demands.

Concluding Thoughts

If you have finally decided to build chat functionality into your client app, then undoubtedly, React Native is one of the most popular frameworks. The reason here is its effectiveness in presenting the combined benefits of hybrid and native apps together under one roof. We talked about React Native benefits in a more detailed way in the first part of the article.

As a back-end for a chat app, we decided to opt for Twilio and Sendbird, which we believe are among the best messaging SDKs for React Native applications. They both allow you to configure, build, and integrate chat functionality into any web or mobile app, but still they have different capabilities. We compared the key features that Twilio and Sendbird offer, and the main objective here while deciding which one to choose is your business goals and the chat app functionality you are looking for.

We have tried to keep each and every step of development very simple and sorted while explaining the customization, navigation, and configuration of the chat app with pieces of code. So, hopefully, these two parts of the article will help you build chat functionality into your app using React Native.


Liked the article? Follow UpsilonIT on dev.to for the latest tech know-hows, best software development practices, and insightful case studies from industry experts! Remember: your likes and comments are fuel for our future posts!

Top comments (0)