Building a Twitter Spaces Clone with NativeBase and 100ms

Introduction
As part of community initiatives at NativeBase, the team partnered with 100ms for a workshop on creating a Twitter Spaces clone. This article serves as a companion to the video version.
Bootstrapping the project
NativeBase provides universal templates across all target platforms, drastically reducing setup time. The framework offers a pre-configured, ready-to-extend foundation for development.
Following the NativeBase Installation guide, developers can begin with the "react-native" template by executing a few simple commands.
Building the Screens
The demo application features two screens: a home screen displaying live spaces and a detailed space view. The home screen utilizes an attractive card component showing multiple details.
SpaceCard Component
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>
);
}Everything is a token
Components in NativeBase leverage a comprehensive design system with professional, tested styling tokens. This extensibility supports brand identity customization. Developers can use tokens like w="full", bg="fuchsia.800", overflow="hidden", and borderRadius="16" through "Utility Props," providing superior developer experience compared to traditional React Native StyleSheet approaches.
Color Modes and Accessibility
NativeBase provides built-in light and dark mode support. Using Pseudo Props, props beginning with an underscore, developers control conditional styling. For example:
<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>The framework uses react-native-aria, ensuring all components are accessible by default without additional effort.
Adding Functionality
The 100ms SDK for React Native simplifies connecting static UI screens to functional features. The SDK is straightforward to configure with comprehensive documentation.
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]);
}<>
<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>
</>The process involves joining a room with a specified ID, fetching authentication tokens via API endpoint, constructing an HMSConfig object, and establishing a connection. Once connected, the system fires events when participants join or update their state, enabling reactive UI changes. Full SDK details are available in the documentation.
Final Product
The demonstration produces a functional Twitter Spaces clone with minimal features. Developers can expand this foundation with additional capabilities to create a production-ready, feature-complete application.