Getting started: Pub/Sub with React Native

This guide will get you started with Ably Pub/Sub in a new React Native application built with Expo.

It will take you through the following steps:

  • Create a client and establish a realtime connection to Ably.
  • Attach to a channel and subscribe to its messages.
  • Publish a message to the channel for your client to receive.
  • Join and subscribe to the presence set of the channel.
  • Retrieve the messages you sent in the guide from history.
  • Close a connection to Ably when it is no longer needed.

Prerequisites

  1. Sign up for a free Ably account.
  2. Create a new app and get your API key from the dashboard.
  3. Your API key will need the publish, subscribe, presence and history capabilities.

This step is optional. If you prefer not to use the Ably CLI to interact with your Pub/Sub React Native application, you can still follow along by using multiple browser windows, mobile devices or emulators.

npm install -g @ably/cli
  1. Run the following to log in to your Ably account and set the default app and API key:
ably login

ably apps switch
ably auth keys switch

Create a React Native project

Create a new React Native project using Expo using Nativewind for styling and then navigate to the project folder:

npx rn-new@latest ably-pubsub-react-native --nativewind
cd ably-pubsub-react-native

To open your project on the web, you also need to install the following required dependencies:

npx expo install react-dom react-native-web @expo/metro-runtime

Replace the contents of App.tsx with the following:

React

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

// App.tsx

import { StatusBar } from 'expo-status-bar';
import { Text, View } from 'react-native';

import './global.css';

export default function App() {
  return (
    <View className="mt-8 w-full flex-1 items-center">
      <View className="w-[90%] max-w-[900px] rounded-lg border border-blue-500">
        <View className="w-full rounded-lg bg-gray-100 p-4">
          <Text className="text-center text-lg font-semibold text-blue-500">
            Ably Pub/Sub React Native
          </Text>
        </View>
      </View>
      <StatusBar style="auto" />
    </View>
  );
}

Install Ably Pub/Sub JavaScript SDK

Install the Ably Pub/Sub JavaScript SDK in your React Native project:

npm install ably

Set up AblyProvider

The Ably Pub/Sub SDK provides React hooks and context providers that make it easier to use Pub/Sub features in your React Native components.

The AblyProvider component should be used at the top level of your application, typically in App.tsx. It provides access to the Ably Realtime client for all child components that use Ably Pub/Sub React hooks.

Replace the contents of your App.tsx file with the following code to set up the AblyProvider:

React

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

// App.tsx

import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, Text, View } from 'react-native';

import './global.css';

// Create your Ably Realtime client
const realtimeClient = new Ably.Realtime({
  key: '{{API_KEY}}',
  clientId: 'my-first-client',
});

export default function App() {
  return (
    <AblyProvider client={realtimeClient}>
      <SafeAreaView className="flex-1 bg-white">
        <View className="mt-8 w-full flex-1 items-center">
          <View className="w-[90%] max-w-[900px] rounded-lg border border-blue-500">
            <View className="w-full rounded-lg bg-gray-100 p-4">
              <Text className="text-center text-lg font-semibold text-blue-500">
                Ably Pub/Sub React Native
              </Text>
            </View>
          </View>
          <StatusBar style="auto" />
        </View>
      </SafeAreaView>
    </AblyProvider>
  );
}

Step 1: Connect to Ably

Clients establish a connection with Ably when they instantiate an SDK instance. This enables them to send and receive messages in realtime across channels.

In the Set up AblyProvider section, you added the following code to create an Ably Realtime client:

React

1

2

3

4

const realtimeClient = new Ably.Realtime({
  key: '{{API_KEY}}',
  clientId: 'my-first-client',
});

This code creates a new Realtime client instance, establishing a connection to Ably when your application starts. At the minimum you need to provide an authentication mechanism. While using an API key is fine for the purposes of this guide, you should use token authentication in production environments. A clientId ensures the client is identified, which is required to use certain features, such as presence.

To monitor the Ably connection state within your application, create a component that uses the useConnectionStateListener() hook provided by the Ably Pub/Sub SDK. This hook must be nested inside an AblyProvider, so the component must be placed within the AblyProvider in your application.

In your project, create a new file components/ConnectionState.tsx with the following content:

React

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

// components/ConnectionState.tsx

// React hooks are exported from the 'ably/react' path of the 'ably' package.
import { useAbly, useConnectionStateListener } from 'ably/react';
import { Text } from 'react-native';
import { useState } from 'react';

export function ConnectionState() {
  // The useAbly hook returns the Ably Realtime client instance provided by the AblyProvider
  const ably = useAbly();
  const [connectionState, setConnectionState] = useState(ably.connection.state);

  // useConnectionStateListener hook listens for changes in connection state
  useConnectionStateListener((stateChange) => {
    setConnectionState(stateChange.current);
  });

  return <Text className="mt-4 text-center">Connection: {connectionState}!</Text>;
}

Update your App component in the App.tsx file to include the ConnectionState component:

React

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

// App.tsx

import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, Text, View } from 'react-native';
import { ConnectionState } from './components/ConnectionState';

import './global.css';

// Create your Ably Realtime client
const realtimeClient = new Ably.Realtime({
  key: '{{API_KEY}}',
  clientId: 'my-first-client',
});

export default function App() {
  return (
    <AblyProvider client={realtimeClient}>
      <SafeAreaView className="flex-1 bg-white">
        <View className="mt-8 w-full flex-1 items-center">
          <View className="w-[90%] max-w-[900px] rounded-lg border border-blue-500">
            <View className="w-full rounded-lg bg-gray-100 p-4">
              <Text className="text-center text-lg font-semibold text-blue-500">
                Ably Pub/Sub React Native
              </Text>
              {/* Add ConnectionState here */}
              <ConnectionState />
            </View>
          </View>
          <StatusBar style="auto" />
        </View>
      </SafeAreaView>
    </AblyProvider>
  );
}

Now start your application by running:

npm run start

Open your React Native app on a physical device by scanning the QR code in your console, or run it locally using the iOS Simulator, Android Emulator, or on the web by typing i, a, or w respectively in your console.

You should see the connection state displayed in your UI (e.g., Connection: connected!). You can also inspect connection events in the dev console of your app.

Step 2: Subscribe to a channel and publish a message

Messages contain the data that a client is communicating, such as a short 'hello' from a colleague, or a financial update being broadcast to subscribers from a server. Ably uses channels to separate messages into different topics, so that clients only ever receive messages on the channels they are subscribed to.

ChannelProvider

Now that you're connected to Ably, you can create and manage channels using the ChannelProvider component from the Ably Pub/Sub SDK. This component must be nested within the AblyProvider described above.

Update your main App component to include the ChannelProvider:

React

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

// App.tsx

import * as Ably from 'ably';
import { AblyProvider, ChannelProvider } from 'ably/react';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, Text, View } from 'react-native';
import { ConnectionState } from './components/ConnectionState';

import './global.css';

// Create your Ably Realtime client
const realtimeClient = new Ably.Realtime({
  key: '{{API_KEY}}',
  clientId: 'my-first-client',
});

export default function App() {
  return (
    <AblyProvider client={realtimeClient}>
      {/* Wrap components with ChannelProvider */}
      <ChannelProvider channelName="my-first-channel">
        ...
      </ChannelProvider>
    </AblyProvider>
  );
}

Subscribe to a channel

Use the useChannel() hook within the ChannelProvider component to subscribe to incoming messages on a channel. This hook also provides access to a channel instance and a publish method for sending messages.

In your project, create a new file called components/Messages.tsx and add new components called Messages and MessageView:

React

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

// components/Messages.tsx

import type { Message } from 'ably';
import { useChannel } from 'ably/react';
import { useState } from 'react';
import { ScrollView, Text, View } from 'react-native';

function MessageView({ message }: { message: Message }) {
  // Displays an individual message
  const isMine = message.clientId === 'my-first-client';
  return (
    <View className={`my-1 rounded px-2 py-1 shadow-sm ${isMine ? 'bg-green-100' : 'bg-blue-50'}`}>
      <Text className="text-gray-800">{message.data}</Text>
    </View>
  );
}

export function Messages() {
  const [messages, setMessages] = useState<Message[]>([]);

  // The useChannel hook subscribes to messages on the channel
  useChannel('my-first-channel', (message) => {
    setMessages((prevMessages) => [...prevMessages, message]);
  });

  return (
    <View className="h-[200px] w-full rounded-lg lg:h-[400px]">
      <ScrollView className="flex-1 p-4">
        {messages.map((msg: Message) => (
          <MessageView key={msg.id} message={msg} />
        ))}
      </ScrollView>
    </View>
  );
}

Next, update your main App component in the App.tsx file to include the Messages component within the ChannelProvider:

React

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

// App.tsx

import * as Ably from 'ably';
import { AblyProvider, ChannelProvider } from 'ably/react';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, Text, View } from 'react-native';
import { ConnectionState } from './components/ConnectionState';
import { Messages } from './components/Messages';

import './global.css';

// Create your Ably Realtime client
const realtimeClient = new Ably.Realtime({
  key: '{{API_KEY}}',
  clientId: 'my-first-client',
});

export default function App() {
  return (
    <AblyProvider client={realtimeClient}>
      <ChannelProvider channelName="my-first-channel">
        <SafeAreaView className="flex-1 bg-white">
          <View className="mt-8 w-full flex-1 items-center">
            <View className="w-[90%] max-w-[900px] rounded-lg border border-blue-500">
              <View className="w-full rounded-lg bg-gray-100 p-4">
                <Text className="text-center text-lg font-semibold text-blue-500">
                  Ably Pub/Sub React Native
                </Text>
                <ConnectionState />
              </View>

              <View className="flex-column justify-evenly lg:flex-row">
                <View className="lg:w-3/4">
                  {/* Your Messages component should go here */}
                  <Messages />
                </View>
              </View>
            </View>
            <StatusBar style="auto" />
          </View>
        </SafeAreaView>
      </ChannelProvider>
    </AblyProvider>
  );
}

You've successfully created a channel instance and set up a listener to receive messages. You can test this immediately by publishing messages using the Ably CLI:

ably channels publish my-first-channel 'Hello from CLI!'

Publish a message

You can publish messages in your React Native app using the publish method provided by the useChannel() hook.

Update your components/Messages.tsx file to include message publishing:

React

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

// components/Messages.tsx

import type { Message } from 'ably';
import { useChannel } from 'ably/react';
import { useState } from 'react';
import { ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';

function MessageView({ message }: { message: Message }) {
  // Displays an individual message
  const isMine = message.clientId === 'my-first-client';
  return (
    <View className={`my-1 rounded px-2 py-1 shadow-sm ${isMine ? 'bg-green-100' : 'bg-blue-50'}`}>
      <Text className="text-gray-800">{message.data}</Text>
    </View>
  );
}

export function Messages() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [inputValue, setInputValue] = useState('');

  // useChannel hook also provides a publish method
  const { publish } = useChannel('my-first-channel', (message) => {
    setMessages((prevMessages) => [...prevMessages, message]);
  });

  // Function to handle publishing messages
  const handlePublish = () => {
    if (!inputValue.trim()) return;
    publish('my-first-messages', inputValue.trim()).catch((err) =>
      console.error('Error publishing message', err)
    );
    setInputValue('');
  };

  return (
    <View className="h-[200px] w-full rounded-lg lg:h-[400px]">
      <ScrollView className="flex-1 p-4">
        {messages.map((msg: Message) => (
          <MessageView key={msg.id} message={msg} />
        ))}
      </ScrollView>
      <View className="mb-2 mt-auto flex-row items-center border-t border-gray-200 p-2">
        <TextInput
          placeholder="Type your message..."
          className="flex-1 rounded border border-gray-400 bg-white p-2"
          value={inputValue}
          onChangeText={setInputValue}
          onSubmitEditing={handlePublish}
        />
        <TouchableOpacity
          className="ml-2 items-center justify-center rounded bg-blue-500 px-4 py-2"
          onPress={handlePublish}>
          <Text className="text-white">Publish</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

Your application now supports publishing realtime messages! Type a message and press "Publish" to see it appear in your UI. Open the app on a second device or simulator to see clients interacting with each other in realtime, or publish messages using the Ably CLI:

ably channels publish my-first-channel 'Hello from CLI!'

Messages from the CLI will appear in your UI in a different color to the ones you sent from the app.

Step 3: Join the presence set

Presence enables clients to be aware of one another if they are present on the same channel. You can then show clients who else is online, provide a custom status update for each, and notify the channel when someone goes offline.

Use the usePresence() and usePresenceListener() hooks provided by the Ably Pub/Sub SDK to interact with the presence feature in your React Native application. The usePresence() hook enables a client to join the presence set on a channel and update their presence status. The usePresenceListener() hook lets you subscribe to presence changes on a channel.

The usePresenceListener() hook also returns an object containing the presenceData array, which holds current presence data on the channel.

Create a new file called components/PresenceStatus.tsx with the following content:

React

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

// components/PresenceStatus.tsx

// 'ably/react' exports hooks for working with presence on a channel
import { usePresence, usePresenceListener } from 'ably/react';
import { Text, View } from 'react-native';

export function PresenceStatus() {
  // Enter the current client into the presence set with an optional status
  usePresence('my-first-channel', { status: "I'm here!" });

  // Subscribe to presence updates on the channel
  const { presenceData } = usePresenceListener('my-first-channel');

  return (
    <View className="w-full bg-white px-4 py-2">
      <Text className="mb-2 border-b border-gray-200 pb-2 text-center text-green-700">
        Present: {presenceData.length}
      </Text>

      <View>
        {presenceData.map((member, idx) => (
          <View key={idx} className="mb-2 flex-row items-center">
            <View className="mr-2 h-2 w-2 rounded-full bg-green-500" />
            <Text className="text-gray-800">
              {member.clientId}
              {member.data?.status ? ` (${member.data.status})` : ''}
            </Text>
          </View>
        ))}
      </View>
    </View>
  );
}

Add the PresenceStatus component to your main App component in App.tsx as follows:

React

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

// App.tsx

// Add PresenceStatus import
import { PresenceStatus } from './components/PresenceStatus';

// Other imports and realtimeClient initialization

export default function App() {
  return (
    <AblyProvider client={realtimeClient}>
      <ChannelProvider channelName="my-first-channel">
        <SafeAreaView className="flex-1 bg-white">
          <View className="mt-8 w-full flex-1 items-center">
            <View className="w-[90%] max-w-[900px] rounded-lg border border-blue-500">
              <View className="w-full rounded-lg bg-gray-100 p-4">
                <Text className="text-center text-lg font-semibold text-blue-500">
                  Ably Pub/Sub React Native
                </Text>
                <ConnectionState />
              </View>
              <View className="flex-column justify-evenly lg:flex-row">
                <View className="border-blue-500 max-lg:border-b lg:w-1/4 lg:border-r">
                  {/* Your PresenceStatus component should go here */}
                  <PresenceStatus />
                </View>
                <View className="lg:w-3/4">
                  <Messages />
                </View>
              </View>
            </View>
            <StatusBar style="auto" />
          </View>
        </SafeAreaView>
      </ChannelProvider>
    </AblyProvider>
  );
}

The application will now display a list of clients currently present on the channel. The usePresence() hook enters your client into the channel's presence set with an optional status, while the usePresenceListener() hook subscribes to presence updates. Your current client ID should appear in the list of online users.

You can have another client join the presence set using the Ably CLI:

ably channels presence enter my-first-channel --client-id "my-cli" --data '{"status":"From CLI"}'

Step 4: Retrieve message history

You can retrieve previously sent messages using the history feature. Ably stores all messages for 2 minutes by default in the event a client experiences network connectivity issues. This can be extended for longer if required.

Although the Ably Pub/Sub SDK does not provide a specific hook for retrieving message history, you can use the useChannel() hook to get a RealtimeChannel instance and then call its history() method to retrieve messages recently published to the channel.

Update your components/Messages.tsx file to include the new useEffect within your existing Messages component:

React

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

// components/Messages.tsx

// Update your react import to have `useEffect` as well.
import { useState, useEffect } from 'react';

// MessageView component remains unchanged

export function Messages() {
  // State management remains the same

  // Include channel from useChannel hook
  const { channel, publish } = useChannel('my-first-channel', (message) => {
    setMessages((prevMessages) => [...prevMessages, message]);
  });

  // handlePublish function remains unchanged

  useEffect(() => {
    async function loadHistory() {
      try {
        // Retrieve the last 5 messages from history
        const history = await channel.history({ limit: 5 });
        // History responses are returned in reverse chronological order (newest first)
        // Reverse the array to show the latest messages at the bottom in the UI
        const messagesFromHistory = history.items.reverse();
        // Update the state with retrieved messages
        setMessages(messagesFromHistory);
      } catch (error) {
        console.error('Error loading message history:', error);
      }
    }

    loadHistory();
  }, [channel]);

  // Return remains unchanged
  return ();
}

Test this feature with the following steps:

  1. Publish several messages using your application UI, or send messages from another client using the Ably CLI:
ably channels publish --count 5 my-first-channel "Message number {{.Count}}"
  1. Refresh the app. This will cause the Messages component to mount again and call the channel.history() method.
  2. You should see the last 5 messages displayed in your UI, ordered from oldest to newest at the bottom:
JSON

1

2

3

4

5

Message number 1
Message number 2
Message number 3
Message number 4
Message number 5

Step 5: Close the connection

Connections are automatically closed approximately two minutes after the last channel is detached. However, explicitly closing connections when they're no longer needed is good practice to help save costs and clean up listeners.

React

1

2

3

4

5

6

7

const realtimeClient = useAbly();

const handleDisconnect = () => {
  realtimeClient.connection.close();
};

// Call handleDisconnect when needed

This ensures the connection is closed when your component unmounts, freeing resources and removing listeners.

Next steps

Continue to explore the Ably Pub/Sub documentation with React as the selected language:

Read more about the concepts covered in this guide:

You can also explore the Ably CLI further, or visit the Pub/Sub API references.

Select...