Building a Twitter Spaces Clone with NativeBase and 100ms
Tags: react, reactnative, clones, twitterspaces
Published: 2022-02-02 11:47:55

**Introduction**

As part of our community initiatives at [NativeBase](https://nativebase.io/?utm_source=Devto&utm_medium=Social&utm_campaign=Article&utm_id=Main+Website), we partnered with [100ms](https://www.100ms.live/) to do a workshop on “Building a Twitter Spaces Clone,” which you can find in the [video version](https://www.youtube.com/watch?v=Clfkd-U3MFI&t=19s). This article is written as a follow-along reading for the same.

**Bootstrapping the project**

One of the great things about using NativeBase is its universal character. You get a template for all the possible target platforms you might be thinking of building an app. This means that the bootstrapping time is reduced drastically. You also get an all-configured, basic app that is ready to be extended.
 
Following the NativeBase Installation guide https://docs.nativebase.io/installation, we will start by using the “react-native” template, and it's as easy as copy-pasting a few commands described in the instructions.

**Building the Screens**

The demo app we are building has two screens. You see the home screen when you launch the app. This screen shows you all the live spaces. The card component on this screen is attractive. It has details of showing several things and is of moderate complexity.

Let's look at how NativeBase makes building UI's like this a cake-walk. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7pxd2o5cn1wunkkvd6ww.png)

**SpaceCard Component**

```javascript

import React from 'react';
import { Box, Text, HStack, Avatar, Pressable } from 'native-base';

export default function (props) {
  return (
    <Pressable
      w="full"
      bg="fuchsia.800"
      overflow="hidden"
      borderRadius="16"
      onPress={props.onPress}
    >
      <Text px="4" my="4" fontSize="md" color="white">
        Live
      </Text>
      <Text w="80%" pl="4" mb="4" fontSize="xl" color="white">
        Building a Twitter Space Clone in React Native using NativeBase and
        100ms
      </Text>
      <HStack p="4" bg="fuchsia.900" space="4">
        <Box flexDirection="row" justifyContent="center" alignItems="center">
          <Avatar
            size="sm"
            alignSelf="center"
            bg="green.200"
            source={{
              uri: 'https://pbs.twimg.com/profile_images/1188747996843761665/8CiUdKZW_400x400.jpg',
            }}
          >
            VB
          </Avatar>
          <Box ml="4">
            <Text fontSize="sm" color="white">
              Vipul Bhardwaj
            </Text>
            <Text fontSize="sm" color="white">
              SSE @GeekyAnts
            </Text>
          </Box>
        </Box>
        <Box flexDirection="row" justifyContent="center" alignItems="center">
          <Avatar
            size="sm"
            alignSelf="center"
            bg="green.200"
            source={{
              uri: 'https://pbs.twimg.com/profile_images/1188747996843761665/8CiUdKZW_400x400.jpg',
            }}
          >
            HO
          </Avatar>
          <Box ml="4">
            <Text fontSize="sm" color="white">
              Host
            </Text>
            <Text fontSize="sm" color="white">
              SE @100ms
            </Text>
          </Box>
        </Box>
      </HStack>
    </Pressable>
  );
}
```
Yup, that's it. That's the whole code. This is how easy NativeBase makes everything🤯. 

Let's look at the code in detail and learn about some of the tiny details that make it even more awesome.

**Everything is a token**

Every component in NativeBase is styled using its comprehensive, professionally designed, and tested Design System, which was created to be extensible to represent the brand identity of your app. This allows you to use tokens available in the [NativeBase theme](https://docs.nativebase.io/default-theme).

And thus, we can use values like `w="full"`, `bg="fuchsia.800"` , `overflow="hidden"` , `borderRadius="16"` all of which are tokens assigned to props. This style of passing styles props as individual values is known as “[Utility Props](https://docs.nativebase.io/utility-props)”, which provides great Developer Experience. NativeBase embraces this idea fully, and uses “[Utility Props](https://docs.nativebase.io/utility-props)”, instead of the default `react-native` StyleSheet approach.

**Color Modes and Accessibility**

NativeBase supports both `light` and `dark` mode out of the box, and all the inbuilt components are designed to work with both color modes. But what if you use something other than the default values. With NativeBase, using [Pseudo Props](https://docs.nativebase.io/pseudo-props) this becomes terribly easy. 

Let's look at an example, this is the JSX code for the HomeScreen, notice on line 1, we have `_light`, and `_dark`. In NativeBase, props that start with an underscore are called [pseudo props](https://docs.nativebase.io/pseudo-props) and they are used to control conditional styling. In the case of light and dark modes, you can use these props to provide styles that will only apply when the color mode is light or dark.

Yes, it's that easy to add support for the dark mode to your components. On top of that, NativeBase used `react-native-aria`, so all the components are accessible by default, without you needing to do anything extra.

```javascript
<Box flex="1" _light={{ bg: 'white' }} _dark={{ bg: 'darkBlue.900' }}>
  <VStack space="2" p="4">
    <Heading>Happening Now</Heading>
    <Text>Spaces going on right now</Text>
  </VStack>
  <ScrollView p="4">
    <VStack space="8">
      <SpaceCard
        onPress={() =>
          navigation.navigate('Space', {
            roomID: 'your-room-id-here',
          })
        }
      />   
    </VStack>
  </ScrollView>
</Box>
```
**Adding Functionality**

We use the 100ms SDK for `react-native`, which makes it extremely easy to get your add from just a collection of UI screens with static data to a full-blown functional app. The SDK is easy to set up and the documentation is great.

```javascript
const fetchToken = async ({ roomID, userID, role }) => {
  const endPoint =
    'https://prod-in.100ms.live/hmsapi/geekyants.app.100ms.live/api/token';

  const body = {
    room_id: roomID,
    user_id: userID,
    role: role,
  };

  const headers = {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  };

  const response = await fetch(endPoint, {
    method: 'POST',
    body: JSON.stringify(body),
    headers,
  });

  const result = await response.json();
  return result;
};

async function joinRoom(hmsInstance, roomID, userID) {
  if (!hmsInstance) {
    console.error('HMS Instance not found');
    return;
  }

  const { token } = await fetchToken({
    roomID,
    userID,
    role: 'speaker',
  });

  const hmsConfig = new HMSConfig({ authToken: token, username: userID });

  hmsInstance.join(hmsConfig);
}

export default function Space({ navigation, route }) {
  const hmsInstance = useContext(HMSContext);
  const [isMute, setMute] = useState(false);
  const [participants, setParticipants] = useState([]);

  const userID = useRef('demouser').current;
  const roomID = useRef(route.params.roomID).current;

  useEffect(() => {
    if (hmsInstance) {
      hmsInstance.addEventListener(HMSUpdateListenerActions.ON_ERROR, (data) =>
        console.error('ON_ERROR_HANDLER', data)
      );

      hmsInstance.addEventListener(
        HMSUpdateListenerActions.ON_JOIN,
        ({ room, localPeer, remotePeers }) => {
          const localParticipant = {
            id: localPeer?.peerID,
            name: localPeer?.name,
            role: localPeer?.role?.name,
            avatar: (
              <Circle w="12" h="12" p="2" bg="blue.600">
                {localPeer?.name?.substring(0, 2)?.toLowerCase()}
              </Circle>
            ),
            isMute: localPeer?.audioTrack?.isMute(),
          };

          const remoteParticipants = remotePeers.map((remotePeer) => {
            return {
              id: remotePeer?.peerID,
              name: remotePeer?.name,
              role: remotePeer?.role?.name,
              avatar: (
                <Circle w="12" h="12" p="2" bg="blue.600">
                  {remotePeer?.name?.substring(0, 2)?.toLowerCase()}
                </Circle>
              ),
              isMute: remotePeer?.audioTrack?.isMute(),
            };
          });

          setParticipants([localParticipant, ...remoteParticipants]);
        }
      );

      hmsInstance.addEventListener(
        HMSUpdateListenerActions.ON_ROOM_UPDATE,
        (data) => console.log('ON ROOM UPDATE', data)
      );

      hmsInstance?.addEventListener(
        HMSUpdateListenerActions.ON_PEER_UPDATE,
        ({ localPeer, remotePeers }) => {
          const localParticipant = {
            id: localPeer?.peerID,
            name: localPeer?.name,
            role: localPeer?.role?.name,
            avatar: (
              <Circle w="12" h="12" p="2" bg="blue.600">
                {localPeer?.name?.substring(0, 2)?.toLowerCase()}
              </Circle>
            ),
            isMute: localPeer?.audioTrack?.isMute(),
          };

          const remoteParticipants = remotePeers.map((remotePeer) => {
            return {
              id: remotePeer?.peerID,
              name: remotePeer?.name,
              role: remotePeer?.role?.name,
              avatar: (
                <Circle w="12" h="12" p="2" bg="blue.600">
                  {remotePeer?.name?.substring(0, 2)?.toLowerCase()}
                </Circle>
              ),
              isMute: remotePeer?.audioTrack?.isMute(),
            };
          });

          setParticipants([localParticipant, ...remoteParticipants]);
        }
      );

      hmsInstance?.addEventListener(
        HMSUpdateListenerActions.ON_TRACK_UPDATE,
        ({ localPeer, remotePeers }) => {
          const localParticipant = {
            id: localPeer?.peerID,
            name: localPeer?.name,
            role: localPeer?.role?.name,
            avatar: (
              <Circle w="12" h="12" p="2" bg="blue.600">
                {localPeer?.name?.substring(0, 2)?.toLowerCase()}
              </Circle>
            ),
            isMute: localPeer?.audioTrack?.isMute(),
          };

          const remoteParticipants = remotePeers.map((remotePeer) => {
            return {
              id: remotePeer?.peerID,
              name: remotePeer?.name,
              role: remotePeer?.role?.name,
              avatar: (
                <Circle w="12" h="12" p="2" bg="blue.600">
                  {remotePeer?.name?.substring(0, 2)?.toLowerCase()}
                </Circle>
              ),
              isMute: remotePeer?.audioTrack?.isMute(),
            };
          });

          setParticipants([localParticipant, ...remoteParticipants]);
        }
      );
    }

    joinRoom(hmsInstance, roomID, userID);
  }, [hmsInstance, roomID, userID]);
}
```

```javascript
<>
  <VStack
    p="4"
    flex="1"
    space="4"
    _light={{ bg: "white" }}
    _dark={{ bg: "darkBlue.900" }}
  >
    <HStack ml="auto" alignItems="center">
      <IconButton
        variant="unstyled"
        icon={<HamburgerIcon _dark={{ color: "white" }} size="4" />}
      />
      <Button variant="unstyled">
        <Text fontSize="md" fontWeight="bold" color="red.600">
          Leave
        </Text>
      </Button>
    </HStack>
    <Text fontSize="xl" fontWeight="bold">
      Building a Twitter Space Clone in React Native using NativeBase and 100ms
    </Text>
    <FlatList
      numColumns={4}
      ListEmptyComponent={<Text>Loading...</Text>}
      data={participants}
      renderItem={({ item }) => (
        <VStack w="25%" p="2" alignItems="center">
          {item.avatar}
          <Text numberOfLines={1} fontSize="xs">
            {item.name}
          </Text>
          <HStack alignItems="center" space="1">
            {item.isMute && (
              <Image
                size="3"
                alt="Peer is mute"
                source={require("../icons/mute.png")}
              />
            )}
            <Text numberOfLines={1} fontSize="xs">
              {item.role}
            </Text>
          </HStack>
        </VStack>
      )}
      keyExtractor={(item) => item.id}
    />
  </VStack>
  <HStack
    p="4"
    zIndex="1"
    safeAreaBottom
    borderTopWidth="1"
    alignItems="center"
    _light={{ bg: "white" }}
    _dark={{ bg: "darkBlue.900" }}
  >
    <VStack space="2" justifyContent="center" alignItems="center">
      <Pressable
        onPress={() => {
          hmsInstance.localPeer.localAudioTrack().setMute(!isMute);
          setMute(!isMute);
        }}
      >
        <Circle p="2" borderWidth="1" borderColor="coolGray.400">
          {isMute ? (
            <Image
              size="8"
              key="mic-is-off"
              alt="mic is off"
              resizeMode={"contain"}
              source={require("../icons/mic-mute.png")}
            />
          ) : (
            <Image
              size="8"
              key="mic-is-on"
              alt="mic is on"
              resizeMode={"contain"}
              source={require("../icons/mic.png")}
            />
          )}
        </Circle>
      </Pressable>
      <Text fontSize="md">{isMute ? "Mic is off" : "Mic is on"}</Text>
    </VStack>
    <HStack ml="auto" mr="4" space="5">
      <Image
        size="7"
        alt="Participant Icon"
        source={require("../icons/users.png")}
      />
      <Image
        size="7"
        alt="Emojie Icon"
        source={require("../icons/heart.png")}
      />
      <Image size="7" alt="Share Icon" source={require("../icons/share.png")} />
      <Image
        size="7"
        alt="Tweet Icon"
        source={require("../icons/feather.png")}
      />
    </HStack>
  </HStack>
</>
```

We first join the room with room id. Then we fetch the authentication tokens by hitting the URL and creating an `HMSConfig` object, which we will use to connect to the room. Once we establish a connection, we will get events based on calls when things happen in the room.

For example, when some peer/user joins the room, we will get an event, and based on that, we can change the state of our data, which will lead to changes reflected in the UI. You can read more about it on the SDK and all the details of different things in the SDK documentation (‣)

**Final Product**

There we have it, a working demo of a feature minimal Twitter spaces clone. You can add many features to extend this and build an incredibly cool and feature-rich app ready for use in the real world 🙂. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3dv7bk222nuxoi77jq2d.gif)