Build Your First App: Tesla Clone with React Native

Vadim Savin profile picture
Vadim SavinMar 1, 2024

In this tutorial, we will build a clone of the Tesla app in just a few hours. This tutorial is perfect for beginners who want to learn the basics of app development and React Native, as we will go step-by-step and explain everything on our way. By the end, you'll have a fully functioning app that you can customize and build upon. So, let's get started and build your first app together!

Create a brand new Expo app

Let’s start by initializing a new Expo project by running:

BASH
npx create-expo-app@latest -e with-router TeslaApp

After that’s done, open the application folder with your editor of choice.

To run the application, we have to open a new terminal and navigate to the application directory. If you are using the terminal from VScode, the terminal automatically opens in the current directory.

Now, let’s start the development server by running

BASH
npm start

We should see the expo dev server. From there, we can either scan the QR code with our physical device using the Expo Go app or run the application on an iOS simulator by pressing i or android emulator by pressing a.

New expo router project

App directory

As we are using Expo router, we will have to create a new directory called app in the root of our project for all our screens.

Expo Router is a file-based navigation system, and for every file inside the app directory, expo router will automatically create a route for it.

If we press the button "touch app/index.js" from the application, it will automatically create the app folder with the main screen inside app/index.js, and the app should automatically re-render to display the new screen. How cool is that 🤩

Let’s open this file, and start working on our app.

Tesla Home screen

Let’s start working on the Tesla home page.

Tesla home screen

That looks beautiful 😍 

Where do we start?

Let’s break down the UI into smaller components. This will help us easier structure and build the UI.

Tesla Home screen breakdown

Now, if we describe in words what we see on the home screen, it will make more sense:

  1. Header
    1. Model name, charge status
    2. Profile icon
  2. Car image
  3. Controls
    1. A list of icons
  4. List of options
    1. List options
      1. Option Icon
      2. Option name
      3. Open Icon

Home screen header

First things first: let’s change the background color of the main container to a dark color: backgroundColor: '#161818'. That’s already better 👌

Let’s open app/index.js and start building our screen from the top, starting with the header.

The header is a container made up of 2 columns. On the left, we have information about the car, and on the right, we have an icon.

JAVASCRIPT
import { StyleSheet, Text, View } from 'react-native';
import { FontAwesome } from '@expo/vector-icons';
export default function Page() {
return (
<View style={styles.container}>
<View style={styles.header}>
<View>
<Text style={styles.title}>My model S</Text>
<Text style={styles.subtitle}>Parked</Text>
</View>
<FontAwesome name="user-circle" size={30} color="gray" />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
backgroundColor: '#161818',
},
header: {
marginTop: 50,
flexDirection: 'row',
justifyContent: 'space-between',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#eee',
marginBottom: 8,
},
subtitle: {
fontWeight: '500',
color: 'gray',
},
});

Working with Images in React Native

The next step is to render the Tesla Car image on the screen.

For that, we will use the <Image> component imported from react-native and an actual png image that we will import from the assets.

JAVASCRIPT
import { Image } from 'react-native';
import car from '../assets/images/car.png';

Let’s render the <Image> component right after the header container, and send the car image as the source

JAVASCRIPT
<Image source={car} style={styles.image} resizeMode="contain" />

For the styles, we will make the image full width of the screen and will limit the height to 300 pixels

JAVASCRIPT
image: {
width: '100%',
height: 300,
},

That’s it. Our app already starts looking better.

Controls

The next step on our Home screen is the row with all the quick controls displayed as icons.

First, let’s import all the Icon families from @expo/vector-icons

JAVASCRIPT
import {
FontAwesome,
Entypo,
MaterialCommunityIcons,
FontAwesome5,
Ionicons,
} from '@expo/vector-icons';

And now, render them inside a container

JAVASCRIPT
<View style={styles.controls}>
<Entypo name="lock" size={26} color="gray" />
<MaterialCommunityIcons name="fan" size={26} color="gray" />
<FontAwesome5 name="bolt" size={26} color="gray" />
<Ionicons name="car-sport-sharp" size={26} color="gray" />
</View>

To put them in the same row, let’s add the flexDirection style prop to the container styles

JAVASCRIPT
controls: {
flexDirection: 'row',
justifyContent: 'space-around',
},

List of options

Before we handle the list of options, let’s start by rendering one option row.

One option row is made of an icon, the name of the option, and on the right the open icon.

JAVASCRIPT
<View style={styles.optionRow}>
<MaterialCommunityIcons name="car" size={26} color="gray" />
<Text style={styles.optionText}>Controls</Text>
<MaterialIcons
name="keyboard-arrow-right"
size={24}
color="gray"
style={{ marginLeft: 'auto' }}
/>
</View>

And the styles for the container and the name

JAVASCRIPT
optionRow: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20
},
optionText: {
color: '#eee',
fontSize: 18,
fontWeight: 'bold',
marginLeft: 10,
},

Now that can render one option item, we should put it together and render a list of options.

Firstly, we will import the data that contains the information on all the options we have to display.

JAVASCRIPT
import menuOptions from '../assets/menuOptions';

To render them, we will use a <FlatList> component. A Flatlist helps us render infinite scrollable lists. We just tell the FlatList what data we want to render, which should be an array, and also how to render each item in that arrow.

JAVASCRIPT
<FlatList
data={menuOptions}
renderItem={({ item }) => (
<View style={styles.optionRow}>
<MaterialCommunityIcons
name={item.iconName}
size={26}
color="gray"
/>
<Text style={styles.optionText}>{item.name}</Text>
<MaterialIcons
name="keyboard-arrow-right"
size={24}
color="gray"
style={{ marginLeft: 'auto' }}
/>
</View>
)}
/>

Custom components

Our Home screen file is getting bigger and harder to manage.

To make sure that our app remains easy to maintain and easy to scale with new features that we will add, we should start extracting chunks of code into custom components.

Let’s start by creating a custom component for our Menu Option list item inside components/MenuOption.js

JAVASCRIPT
import { View, Text, StyleSheet } from 'react-native';
import { MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
const MenuOption = ({ item }) => {
return (
<View style={styles.optionRow}>
<MaterialCommunityIcons name={item.iconName} size={26} color="gray" />
<Text style={styles.optionText}>{item.name}</Text>
<MaterialIcons
name="keyboard-arrow-right"
size={24}
color="gray"
style={{ marginLeft: 'auto' }}
/>
</View>
);
};
const styles = StyleSheet.create({
optionRow: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20,
},
optionText: {
color: '#eee',
fontSize: 18,
fontWeight: 'bold',
marginLeft: 10,
},
});
export default MenuOption;

And back inside app/inde.jx, our FlatList will use this custom component to render items

JAVASCRIPT
<FlatList data={menuOptions} renderItem={MenuOption} />

Assignment: try extracting the controls row that is rendered above the FlatList in a separate component.

Linking

When we press one menu option, we should navigate to a new screen.

For that, we will use the <Link> component from expo-router inside the components/MenuOption

JAVASCRIPT
const MenuOption = ({ item }) => {
return (
<Link href={item.href} asChild>
<Pressable style={styles.optionRow}>
...
</Pressable>
</Link>
);
};

Climate Screen

Let’s create a new page for the page that renders the climate control. For that, create a new file app/climate.js

JAVASCRIPT
import { View, Text } from 'react-native';
const ClimateScreen = () => {
return (
<View>
<Text>climate</Text>
</View>
)
};
export default ClimateScreen;

Now, if you press on the climate option from the home page, you should be redirected to the newly created Climate control page.

If you still see the text "Unmatched Route", double-check the href of the climate option from assets/menuOptions.js to match the name of your file inside the app folder.

Time to practice

Now, it is time for you to practice. For that, try to build the Climate control page. Start by breaking down the page into smaller components, and then one by one, implement them. There is nothing new. You will still have to work with Images, texts, icons, and views.

You will find the image inside the assets folder.

simulator_screenshot_8EA926C7-FE2F-4035-99B5-3708A69206C4.png

Well done! Even if you didn’t manage to make it perfect as it is in the screenshot, I think you still managed to learn a lot by doing it yourself.

Here you can see the way I implemented it

JAVASCRIPT
import { View, Text, Image, StyleSheet, Pressable } from 'react-native';
import climateImage from '../assets/images/climate.png';
import { MaterialCommunityIcons, Entypo } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
const ClimateScreen = () => {
const router = useRouter();
return (
<View style={styles.container}>
<Image source={climateImage} style={styles.image} resizeMode="cover" />
<Pressable onPress={() => router.back()} style={styles.back}>
<Entypo name="chevron-left" size={24} color="white" />
</Pressable>
<View style={styles.footer}>
<Text style={styles.label}>Interior 74°F - Exterior 66°F</Text>
<View style={styles.controlsRow}>
<View style={styles.iconButtonContainer}>
<MaterialCommunityIcons name="power" size={42} color="white" />
<Text style={styles.iconButtonText}>On</Text>
</View>
<View style={styles.temperatureContainer}>
<Entypo name="chevron-left" size={30} color="gray" />
<Text style={styles.temperatureText}>68°</Text>
<Entypo name="chevron-right" size={30} color="gray" />
</View>
<View style={styles.iconButtonContainer}>
<MaterialCommunityIcons name="car-door" size={42} color="gray" />
<Text style={styles.iconButtonText}>Vent</Text>
</View>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#161818',
},
back: {
position: 'absolute',
top: 50,
left: 25,
backgroundColor: '#2f3030',
padding: 10,
borderRadius: 5,
},
image: {
width: '100%',
height: '65%',
},
footer: {
alignItems: 'center',
padding: 12,
marginBottom: 20,
marginTop: 'auto',
},
label: {
color: 'gray',
fontSize: 18,
fontWeight: '600',
marginVertical: 20,
},
controlsRow: {
flexDirection: 'row',
width: '100%',
justifyContent: 'space-around',
},
temperatureContainer: {
flexDirection: 'row',
alignItems: 'center',
},
temperatureText: {
fontSize: 48,
fontWeight: '300',
color: 'white',
marginHorizontal: 20,
},
iconButtonContainer: {
alignItems: 'center',
},
iconButtonText: {
color: 'white',
fontSize: 18,
fontWeight: '600',
marginTop: 10,
},
});
export default ClimateScreen;

Conclusion

I hope you enjoyed this build as much as I enjoyed putting it together.

If you really want to put your skills to the test, then don’t stop here. Go ahead and implement a couple more screens yourselves and this way you will 10x your React Native skills.


Vadim Savin profile picture

Vadim Savin

Hi 👋 Let me introduce myself

I started my career as a Fullstack Developer when I was 16 y.o.

In search of more freedom, I transitioned to freelancing, which quickly grew into a global software development agency 🔥

Because that was not challenging enough, I started my startup which is used by over 20k users. This experience gave another meaning to being a (notJust) developer 🚀

I am also a proud ex-Amazon SDE and Certified AWS Architect, Developer and SysOps. You are in good hands 👌