LinkedIn UI with React Native

Part of:  Full-stack LinkedIn Clone   

Let’s kickstart the LinkedIn Clone series by building the User Interface using React Native and Expo.

This is a great hands-on tutorial for building cross-platform apps for Web and Mobile with a modern stack:

  • React Native and Expo
  • Expo Router
  • Typescript

Video tutorial

This guide was designed to be used together with the video tutorial. Open it in a new tab, and let’s get started.

Asset Bundle

The Asset bundle contains all the required assets to help you follow along.

Download Asset Bundle

Create a new Expo project

Let’s start by creating a new React Native application using Expo. We will use the tabs template, which comes with Expo Router configures, and a couple of sample screens.

BASH
npx create-expo-app@latest LinkedIn -t tabs@sdk-49

After the project is initialized, let’s open it up in our editor of choice.

Open a terminal, and start the development server with npm start

The next step is to run our app on a device. The easiest way is to download the Expo Go app (available both on Play Market and App Store), and then scan the QR you see in the terminal. That way, you can continue to develop the app and see the hot updates directly on your device.

Optionally, you can run the app on an iOS Simulator by pressing i or on Android Emulator by pressing a. But for this, you have to set up the emulators using Xcode and/or Android Studio.

Clean the template

Let’s clean up a bit the default template to make our work easier later.

  1. Move the folders app, components, and constants inside a new src directory to keep everything close together.
  2. Fix all the imports
  3. Remove unused components

Setup all the Tabs

Let’s set up all the tabs in our Bottom Tab navigator. For every screen we have in the bottom tabs, we have to create a new file inside src/app/(tabs)

  • index.tsx
  • network.tsx
  • new-post.tsx
  • notifications.tsx
  • jobs.tsx

Initially, all these screens will simply render their title, and later we will implement them. You can use this code for the boilerplate

TYPESCRIPT
import { StyleSheet, Text, View } from 'react-native';
export default function NetworkScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Network Screen</Text>
<Text>Coming soon!</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 26,
fontWeight: 'bold',
marginBottom: 10,
},
});

Now, we should see separate tabs for every screen we added, but they are lacking a proper icon and title. We can configure these options inside the src/app/(tabs)/_layout.tsx file, which is responsible for the TabNavigator. In this file, we can drop down to the React Navigation level, and configure the screens using the options property.

💡 All the properties available in React Navigation will also work here because Expo Router is built on top of React Navigation.

We will set the title and the tabBarIcon options for all our tabs.

TYPESCRIPT
<Tabs.Screen
name="network"
options={{
title: "My Network",
tabBarIcon: ({ color }) => <TabBarIcon name="group" color={color} />,
}}
/>
<Tabs.Screen
name="new-post"
options={{
title: "Post",
tabBarIcon: ({ color }) => (
<TabBarIcon name="plus-square" color={color} />
),
}}
/>
<Tabs.Screen
name="notifications"
options={{
title: "Notifications",
tabBarIcon: ({ color }) => <TabBarIcon name="bell" color={color} />,
}}
/>
<Tabs.Screen
name="jobs"
options={{
title: "Jobs",
tabBarIcon: ({ color }) => (
<TabBarIcon name="briefcase" color={color} />
),
}}
/>

Home Feed

Now that we have the basic structure of our application, let’s start working on the main screen which is the Home Feed.

Before we jump into rendering a list of posts, let’s start with rendering just one. As we will need to render posts in different parts of the application, it will be really handy if we create the post as a separate and reusable component.

PostListItem

Create the file for our new components inside src/components/PostListItem.tsx and let’s build the user interface.

Follow the video tutorial for a step-by-step build of this component. Here we have the final result.

TYPESCRIPT
import { View, Text, StyleSheet, Image } from 'react-native';
import { Post } from '@/types';
import { FontAwesome } from '@expo/vector-icons';
type PostListItemProps = {
post: Post;
};
type FooterButtonProp = {
text: string;
icon: React.ComponentProps<typeof FontAwesome>['name'];
};
const FooterButton = ({ text, icon }: FooterButtonProp) => (
<View style={styles.footerButton}>
<FontAwesome name={icon} size={16} color="gray" />
<Text style={styles.footerButtonText}>{text}</Text>
</View>
);
const PostListItem = ({ post }: PostListItemProps) => {
return (
<View style={styles.container}>
<View style={styles.header}>
<Image source={{ uri: post.author.image }} style={styles.userImage} />
<View>
<Text style={styles.userName}>{post.author.name}</Text>
<Text style={styles.position}>{post.author.position}</Text>
</View>
</View>
<Text style={styles.content}>{post.content}</Text>
{post.image && (
<Image source={{ uri: post.image }} style={styles.postImage} />
)}
<View style={styles.footer}>
<FooterButton text="Like" icon="thumbs-o-up" />
<FooterButton text="Comment" icon="comment-o" />
<FooterButton text="Share" icon="share" />
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
maxWidth: 600,
width: '100%',
alignSelf: 'center',
},
header: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
},
userImage: {
width: 50,
aspectRatio: 1,
borderRadius: 25,
marginRight: 10,
},
userName: {
fontWeight: '600',
marginBottom: 5,
},
position: {
fontSize: 12,
color: 'grey',
},
content: {
margin: 10,
marginTop: 0,
},
postImage: {
width: '100%',
aspectRatio: 1,
},
footer: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingVertical: 10,
borderTopWidth: 1,
borderColor: 'lightgray',
},
footerButton: {
flexDirection: 'row',
alignItems: 'center',
},
footerButtonText: {
marginLeft: 5,
color: 'gray',
fontWeight: '600',
},
});
export default PostListItem;

To see the component on the screen, we have to import and render it inside src/app/(tabs)/index.tsx

TYPESCRIPT
import posts from '../../../assets/data/posts.json';
import PostListItem from '@/components/PostListItem';
export default function HomeFeed() {
return <PostListItem post={posts[1]} />;
}

At this point, we should see one post rendered on the screen.

Simulator_Screenshot_-iPhone_14-_2023-07-21_at_12.45.45.png

Infinite scrollable feed using a FlatList

If we can render one post, we can render a list of them. For that, we will use a FlatList component from React Native, that is designed for rendering long lists of data, usually infinite scrollable.

TYPESCRIPT
import { FlatList } from 'react-native';
import posts from '../../../assets/data/posts.json';
import PostListItem from '@/components/PostListItem';
export default function HomeFeed() {
return (
<FlatList
data={posts}
renderItem={({ item }) => <PostListItem post={item} />}
contentContainerStyle={{ gap: 10 }}
showsVerticalScrollIndicator={false}
/>
);
}

Post Details Screen

Every post should have its own screen. This is required if we want to have deep-link to specific posts. For example, if someone likes your posts, when you press on that notification, we want to open the Post details screen.

To set up the screen, create a new file src/app/posts/[id].tsx. The [id] from the file name serves as the dynamic part of the URL. With this approach, if we navigate to /posts/123 then the id will be parsed from the URL and will be 123.

TYPESCRIPT
import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import posts from '../../../assets/data/posts.json';
import PostListItem from '@/components/PostListItem';
const PostDetails = () => {
const { id } = useLocalSearchParams();
const post = posts.find((p) => p.id === id);
if (!post) {
return <Text>Not found</Text>;
}
return <PostListItem post={post} />;
};
export default PostDetails;

Now, if we run the app on the web, and manually change the URL to http://localhost:8081/posts/2, we should see the post displayed.

However, we must navigate to this page when we press on a post inside the feed. Let’s use the <Link> component from Expo Router inside the components/PostListItem.tsx.

Wrap the whole component inside:

TYPESCRIPT
<Link href={`/posts/${post.id}`} asChild>
<Pressable style={styles.container}>
... the rest of your componet
</Pressable>
</Link>

Now when we press on the component inside the feed, we are redirected to the details page.

Profile Screen

We also need a separate screen for the User Profile Screen. Let’s create it inside app/users/[id].tsx. The profile screen is made of:

  • Header
    • Background Image
    • Profile picture
    • Name and Position
    • Connect button
  • About section

simulator_screenshot_DE5BC4F2-F40E-4E9E-9270-8C718F4E67D8.png

Here is the final code for this screen.

TYPESCRIPT
import {
View,
Text,
StyleSheet,
Image,
Pressable,
ScrollView,
} from 'react-native';
import React, { useLayoutEffect, useState } from 'react';
import dummyUser from '../../../assets/data/user';
import { useLocalSearchParams, useNavigation } from 'expo-router';
import { User } from '@/types';
const UserProfile = () => {
const [user, setUser] = useState<User>(dummyUser);
const { id } = useLocalSearchParams();
const navigation = useNavigation();
useLayoutEffect(() => {
navigation.setOptions({ title: user.name });
}, [user]);
return (
<ScrollView>
<View style={styles.headerContainer}>
<Image source={{ uri: user.backImage }} style={styles.backImage} />
<View style={styles.headerContent}>
<Image source={{ uri: user.image }} style={styles.image} />
<Text style={styles.name}>{user.name}</Text>
<Text>{user.position}</Text>
<Pressable style={styles.button}>
<Text style={styles.buttonText}>Connect</Text>
</Pressable>
</View>
</View>
{user.about && (
<View style={styles.container}>
<Text style={styles.title}>About</Text>
<Text>{user.about}</Text>
</View>
)}
<View style={styles.container}>
<Text style={styles.title}>Experience</Text>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
padding: 10,
marginVertical: 5,
backgroundColor: 'white',
},
headerContainer: {
marginBottom: 5,
backgroundColor: 'white',
},
headerContent: {
padding: 10,
},
title: {
fontSize: 18,
fontWeight: '600',
marginVertical: 5,
},
backImage: {
width: '100%',
height: 150,
marginBottom: -60,
},
image: {
width: 100,
aspectRatio: 1,
borderRadius: 100,
borderWidth: 3,
borderColor: 'white',
marginBottom: 10,
},
name: {
fontSize: 24,
fontWeight: '500',
},
button: {
backgroundColor: 'royalblue',
padding: 5,
borderRadius: 100,
alignItems: 'center',
marginVertical: 10,
},
buttonText: {
color: 'white',
fontWeight: '600',
fontSize: 16,
},
});
export default UserProfile;

Experience

To render a list of work-related experience, we have to need an ExperienceListItem component

TYPESCRIPT
import { Experience } from "@/types";
import { View, Text, StyleSheet, Image } from "react-native";
type ExperienceListItemProps = {
experience: Experience;
};
const ExperienceListItem = ({ experience }: ExperienceListItemProps) => {
return (
<View style={styles.container}>
<Image source={{ uri: experience.companyImage }} style={styles.image} />
<View>
<Text style={styles.title}>{experience.title}</Text>
<Text>{experience.companyName}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 5,
paddingBottom: 10,
marginBottom: 10,
borderBottomWidth: 0.5,
borderColor: "lightgray",
flexDirection: "row",
alignItems: "center",
},
image: {
width: 50,
aspectRatio: 1,
marginRight: 5,
},
title: {
fontSize: 16,
fontWeight: "500",
},
});
export default ExperienceListItem;

Inside the profile screen, we can render it by mapping through all the experiences from the user data.

TYPESCRIPT
<View style={styles.container}>
<Text style={styles.title}>Experience</Text>
{user.experience?.map((experience) => (
<ExperienceListItem experience={experience} key={experience.id} />
))}
</View>

Bonus screens

So far, we have built 3 important screens for the Home Feed, Post details, and User Profile.

To make this app complete, we will also need a way to upload posts, maybe leave comments, and search for users and posts.

Let’s build these screens together in the live stream.

Next steps

This was just part one of this series in which we build the user interface of LinkedIn.

To make this app functional, we need a backend for it.

In the next part, we will build a scalable and secure GraphQL API using StepZen and PostgreSQL as a database.


Check other episodes from the  Full-stack LinkedIn Clone  series


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 👌