Instagram Clone Part 2: Create a User Profile Screen
Learn how to use React Native and Firebase to build an Instagram clone complete with a user ID and user profile page.
6 October 2021
In our last post, we discussed how to upload files to Firestore and query a real-time server to fetch images and display them in the app. I have gone a step further and integrated Firebase auth service so that whenever a user uploads a photo, they have to be authenticated, and the posts
collection will contain the reference of the user’s unique identifier uid
.
You can find the complete source code up to this point at this Github repo release.
By the end of this tutorial, you are going to complete another screen called Profile
. Using the Firebase backend and integrated authentication service, you will be able to upload a user’s avatar as well as fetch all of the user’s posts and display them.
One of the important changes to Firebase queries that I have made since the previous post is to add a reference to the user’s uid
. A user’s profile screen will display posts based on this uid
. This not relevant to the Feed
screen since it shows all posts (as in real apps, the feed shows posts from other followed users).
Open src/utils/Firebase.js
and modify the uploadPost()
function. It gets the current user’s uid
from the object returned by firebase.auth().currentUser
. This is how the current user object from Firebase looks like:
{
"displayName": null,
"email": "test@crowdbotics.com",
"emailVerified": false,
"isAnonymous": false,
"metadata": {
"creationTime": 1573196213572,
"lastSignInTime": 1573196213572
},
"phoneNumber": null,
"photoURL": null,
"providerData": [[Object]],
"providerId": "firebase",
"refreshToken": "AEu4IL064RJkHKhU0e3pAjS49hmio4RgkkpgvMXFN7tXTTZcP2PffS1dc57hy2RJQPgMstQWP_LojWUfUsAhDqMWQirlztOKHVuWQgbM27WruKWfsq79KEUkmqyhU2oi-_fJhQazWFomnUfHekuUqGjVZSXGjBS4fOBLbK2GAKIpo6PZYQXZ97jxDvA-cBM3TV8HwH8ak4b-",
"uid": "mrBOaRvdrJPsLkHgJ0uoVI0WyO33"
}
Back to the modification:
uploadPost: post => {
// add this
let user = firebase.auth().currentUser
const id = uuid.v4()
const uploadData = {
// add this
uid: user.uid,
id: id,
postPhoto: post.photo,
postTitle: post.title,
postDescription: post.description,
likes: []
}
return firebase
.firestore()
.collection('posts')
.doc(id)
.set(uploadData)
},
Here is an example of how it looks in the Firestore document.
Under a user’s profile, you’d only want to show posts that are uploaded by the user, unlike the Feed
screen. To do this, start by adding a new query method getUserPosts
in the src/utils/Firebase.js
file.
This method will start by getting the current user. You can then add a where()
method to filter only those posts that contain a field of uid
of the same user who is currently logged in the app.
getUserPosts: () => {
let user = firebase.auth().currentUser
return firebase
.firestore()
.collection('posts')
.where('uid', '==', user.uid)
.get()
.then(function(querySnapshot) {
let posts = querySnapshot.docs.map(doc => doc.data())
return posts
})
.catch(function(error) {
console.log('Error getting documents: ', error)
})
}
The rest is the same as getting all the documents from posts
collection.
To create a user profile, open src/screens/Profile.js
. Right now, let’s use a static user avatar. To get started, import the following statements.
import React, { Component } from 'react'
import { View } from 'react-native'
import { Text, Button, withStyles, Avatar } from 'react-native-ui-kitten'
import { withFirebaseHOC } from '../utils'
import Gallery from '../components/Gallery'
Next, create a class component _Profile
that has a state variable of images
whose value is an empty array. Using this state variable you can later fetch only each photo’s uri
and send it to props to a grid component.
class _Profile extends Component {
state = {
images: []
}
// ...
}
Before you add logic and create UI for this screen, add the following snippet. This wraps the _Profile
class component with a higher order function that allows this component to use Firebase query functions. Also, add necessary styles with another higher order function called withStyles
provided by UI Kitten.
export default Profile = withFirebaseHOC(
withStyles(_Profile, theme => ({
root: {
backgroundColor: theme['color-basic-100'],
marginTop: 60
},
header: {
alignItems: 'center',
paddingTop: 25,
paddingBottom: 17
},
userInfo: {
flexDirection: 'row',
paddingVertical: 18
},
bordered: {
borderBottomWidth: 1,
borderColor: theme['color-basic-400']
},
section: {
flex: 1,
alignItems: 'center'
},
space: {
marginBottom: 3,
color: theme['color-basic-1000']
},
separator: {
backgroundColor: theme['color-basic-400'],
alignSelf: 'center',
flexDirection: 'row',
flex: 0,
width: 1,
height: 42
},
buttons: {
flexDirection: 'row',
paddingVertical: 8
},
button: {
flex: 1,
alignSelf: 'center'
},
text: {
color: theme['color-basic-1000']
}
}))
)
Go back to the _Profile
class to add a function that fetches the post from the query you wrote in the previous section. This function will only add the URI of each image in the state variable images
.
It is going to be an async
function, so to handle promises gracefully, a try/catch
block is used.
fetchPosts = async () => {
try {
const posts = await this.props.firebase.getUserPosts()
let images = posts.map(item => {
return item.postPhoto
})
this.setState({ images })
console.log(this.state.images)
} catch (error) {
console.log(error)
}
}
As soon as the Profile
screen renders, this handler method should run. Thus, use a lifecycle method componentDidMount
.
componentDidMount() {
this.fetchPosts()
}
Next, go to the render
function and destructure the state
object as well as props. The themedStyle
is a prop provided UI kitten library.
render() {
const { images } = this.state
const { themedStyle } = this.props
// ...
}
The render
function is going to return the JSX to display the profile screen. This screen contains a user avatar (which is coming from static image resource URL), a section that shows several posts, follower count, and following count. The demo app does not have the latter two in the database as of now.
The following section is going to show two buttons and, after that, another component called Gallery
that will display a grid of images.
return (
<View style={themedStyle.root}>
<View style={[themedStyle.header, themedStyle.bordered]}>
<Avatar
source={{
uri:
'https://images.unsplash.com/photo-1559526323-cb2f2fe2591b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80'
}}
size='giant'
style={{ width: 100, height: 100 }}
/>
<Text category='h6' style={themedStyle.text}>
Test User
</Text>
</View>
<View style={[themedStyle.userInfo, themedStyle.bordered]}>
<View style={themedStyle.section}>
<Text category='s1' style={themedStyle.space}>
{images.length}
</Text>
<Text appearance='hint' category='s2'>
Posts
</Text>
</View>
<View style={themedStyle.section}>
<Text category='s1' style={themedStyle.space}>
0
</Text>
<Text appearance='hint' category='s2'>
Followers
</Text>
</View>
<View style={themedStyle.section}>
<Text category='s1' style={themedStyle.space}>
0
</Text>
<Text appearance='hint' category='s2'>
Following
</Text>
</View>
</View>
<View style={themedStyle.buttons}>
<Button
style={themedStyle.button}
appearance='ghost'
status='danger'
onPress={this.handleSignout}>
LOGOUT
</Button>
<View style={themedStyle.separator} />
<Button style={themedStyle.button} appearance='ghost' status='danger'>
MESSAGE
</Button>
</View>
<Gallery items={images} />
</View>
)
Here is the output of the above snippet:
You must have noticed in the previous section’s snippet that there is an onPress
attribute on a button with contents LOGOUT
. It accepts a handler method as the value. Add the following handler method in your _Profile
component.
handleSignout = async () => {
try {
await this.props.firebase.signOut()
this.props.navigation.navigate('Auth')
} catch (error) {
console.log(error)
}
}
To make it work, make sure that src/utils/Firebase.js
profile has the following query method.
signOut: () => {
return firebase.auth().signOut()
}
Here is the output:
The images for each user’s profile are shown using a FlatList
component. The grid will have three images in a row; however, you can edit this to your liking. Create a new file Gallery.js
inside the src/components/
directory and import the following statements.
import React, { Component } from 'react'
import {
View,
FlatList,
Dimensions,
StyleSheet,
Image,
TouchableOpacity
} from 'react-native'
The Gallery component receives a prop called items
from the Profile
component. This prop will have a similar data structure.
[
{
uri:
'file:///Users/amanhimself/Library/Developer/CoreSimulator/Devices/8B7FC54D-3BA2-4679-89DC-062DDA882EFD/data/Containers/Data/Application/E3280502-CEA3-48E5-A390-9D9E4182D4E5/tmp/0B64032C-DB63-47E2-81C0-98344F172A7C.jpg'
},
{
uri:
'file:///Users/amanhimself/Library/Developer/CoreSimulator/Devices/8B7FC54D-3BA2-4679-89DC-062DDA882EFD/data/Containers/Data/Application/E3280502-CEA3-48E5-A390-9D9E4182D4E5/tmp/6E9E2631-AA76-4526-945C-98FBF31C6DEE.jpg'
},
{
uri:
'file:///Users/amanhimself/Library/Developer/CoreSimulator/Devices/8B7FC54D-3BA2-4679-89DC-062DDA882EFD/data/Containers/Data/Application/E3280502-CEA3-48E5-A390-9D9E4182D4E5/tmp/05AAAFD6-FB09-47EC-ACD3-7AB1F8562B69.jpg'
}
]
Using Dimensions
from the react-native API, the width of the screen is calculated and stored in itemSize
such that three images are displayed in a row.
Here is the snippet for the Gallery
component so far.
class Gallery extends Component {
constructor(props) {
super(props)
const itemSize = (Dimensions.get('window').width - 12) / 3
this.state = {
data: this.props.items,
itemSize,
total: this.props.items.length
}
}
//...
}
const styles = StyleSheet.create({
images: {
flexDirection: 'row',
paddingHorizontal: 0.5
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: 'white'
}
})
export default Gallery
Make sure you add the styles. Next, add the render()
to display the images on the screen. This component uses FlatList
to display images as you have seen in the Feed
screen in the previous post.
FlatList
requires three mandatory attributes:
data
: an array of datarenderItem
: that contains the JSX for each item in the data arraykeyExtractor
: used to extract a unique key for a given item at the specified indexAnother attribute you are going to use is called numColumns
. It accepts a number as its value and has the functionality to render multiple columns based on that value, and it automatically adds a flexWrap layout. The demo app is going to have three columns.
extractItemKey = index => `${index}`
renderItem = ({ item, index }) => (
<React.Fragment>
<TouchableOpacity onPress={() => alert('add functionality to open')}>
<Image
style={{
width: this.state.itemSize,
height: this.state.itemSize,
margin: 1.5
}}
source={item}
/>
</TouchableOpacity>
</React.Fragment>
)
render() {
return (
<View style={styles.images}>
<FlatList
data={this.state.data}
numColumns={3}
keyExtractor={this.extractItemKey}
renderItem={this.renderItem}
/>
</View>
)
}
Here is the output you will get on the emulator at the end of this section.
Let us write a new Firebase query to fetch user details and display them under the user profile. For example, it can display the correct user name under their profile avatar.
Open src/utils/Firebase.js
and add the following query. This query, based on the current user’s id, will fetch the right document that has the same uid
in the collection users
.
getUserDetails: () => {
let user = firebase.auth().currentUser
return firebase
.firestore()
.collection('users')
.doc(user.uid)
.get()
.then(function(doc) {
let userDetails = doc.data()
return userDetails
})
.catch(function(error) {
console.log('Error getting documents: ', error)
})
}
Next, go back to the src/screens/Profile.js
and update the state with a new userDetails
object.
state = {
images: [],
// add this
userDetails: {}
}
Next, add a new asynchronous method fetchUserDetails
that will update the state object previously created.
fetchUserDetails = async () => {
try {
const userDetails = await this.props.firebase.getUserDetails()
this.setState({ userDetails })
} catch (error) {
console.log(error)
}
}
The results fetched by this asynchronous function are the complete details inside the Firestore document.
Lastly, make sure to invoke this function as soon as the component renders.
componentDidMount() {
// add this
this.fetchUserDetails()
// ...
this.fetchPosts()
}
In the render method, instead of Test User
, use the state variable userDetails.name
to display the correct user name.
// first, update the destructuring
const { images, userDetails } = this.state
// next, add this
<Text category='h6' style={themedStyle.text}>
{userDetails.name}
</Text>
Here is the output:
In this section, you are going to add functionality to let users upload their own avatar image instead of showing a static image for every user. To begin, add the query function uploadAvatar
to upload the image to the Firestore. This query function is going to have the URI to the image as its argument.
Open src/utils/Firebase.js
and add the following:
uploadAvatar: avatarImage => {
let user = firebase.auth().currentUser
return firebase
.firestore()
.collection('users')
.doc(user.uid)
.update({
avatar: avatarImage
})
}
Before you proceed, create a new screen component file EditAvatar.js
in src/screens
directory with some mock output that displays a text.
Open src/navigation/StackNavigator.js
and let us add a stack navigation pattern for the profile screen. Since the edit avatar screen has only one entry point and that is the Profile screen, it is better to perform this step.
import Profile from '../screens/Profile'
import EditAvatar from '../screens/EditAvatar'
Now, export the Profile Navigator
that has its own stack of two screens.
export const ProfileNavigator = createAppContainer(
createStackNavigator({
Profile: {
screen: Profile
},
EditAvatar: {
screen: EditAvatar
}
})
)
The main entry point for the Profile
screen is through the bottom tab. You will have to let the TabNavigator
know about this change. Open src/navigation/TabNavigator.js
and import ProfileNavigator
.
import { FeedNavigator, ProfileNavigator } from './StackNavigator'
Now, replace the value of screen
in Profile
.
Profile: {
screen: ProfileNavigator,
//... rest remains same
}
Open src/screens/Profile.js
and add TouchableOpacity
from react-native
as well as Icon
from UI kitten.
import { View, TouchableOpacity } from 'react-native'
import { Text, Button, withStyles, Avatar, Icon } from 'react-native-ui-kitten'
You are going to add a small edit icon to the user’s avatar. Here is the output to be achieved by the end of this section.
To do so, first, modify the render
method where the Avatar
component is used from UI kitten and add the following in place of it.
The source
attribute will now fetch the image from the userDetails
. If the avatar image exists, it will be shown. The TouchableOpacity
is used to let the user navigate to the EditAvatar
screen.
<View>
<Avatar
source={this.state.userDetails.avatar}
size='giant'
style={{ width: 100, height: 100 }}
/>
<View style={themedStyle.add}>
<TouchableOpacity onPress={this.handleEditAvatarNavigation}>
<Icon name='edit-outline' width={20} height={20} fill='#111' />
</TouchableOpacity>
</View>
</View>
Add the following handler method before render to perform the navigation between two screens.
handleEditAvatarNavigation = () => {
this.props.navigation.navigate('EditAvatar')
}
Lastly, add the following styles to achieve accurate results. Using position: absolute
the desired output can be achieved.
add: {
backgroundColor: '#939393',
position: 'absolute',
bottom: 0,
right: 0,
width: 30,
height: 30,
borderRadius: 15,
alignItems: 'center',
justifyContent: 'center'
}
Open src/screens/EditAvatar.js
and import the following statements.
import React, { Component } from 'react'
import { View, Image } from 'react-native'
import { Button, Text } from 'react-native-ui-kitten'
import ImagePicker from 'react-native-image-picker'
import { withFirebaseHOC } from '../utils'
Using react-native-image-picker
you can let the user select an image. This is the same process you followed while adding an image for the post.
Add the EditAvatar
class component with the following code snippet.
class EditAvatar extends Component {
state = {
avatarImage: null
}
selectImage = () => {
const options = {
noData: true
}
ImagePicker.launchImageLibrary(options, response => {
if (response.didCancel) {
console.log('User cancelled image picker')
} else if (response.error) {
console.log('ImagePicker Error: ', response.error)
} else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton)
} else {
const source = { uri: response.uri }
this.setState({
avatarImage: source
})
}
})
}
onSubmit = async () => {
try {
const avatarImage = this.state.avatarImage
this.props.firebase.uploadAvatar(avatarImage)
this.setState({
avatarImage: null
})
} catch (e) {
console.error(e)
}
}
render() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text category='h2'>Edit Avatar</Text>
<View>
{this.state.avatarImage ? (
<Image
source={this.state.avatarImage}
style={{ width: 300, height: 300 }}
/>
) : (
<Button
onPress={this.selectImage}
style={{
alignItems: 'center',
padding: 10,
margin: 30
}}>
Add an image
</Button>
)}
</View>
<Button
status='success'
onPress={this.onSubmit}
style={{ marginTop: 30 }}>
Add post
</Button>
</View>
)
}
}
export default withFirebaseHOC(EditAvatar)
You will get the following output:
On clicking the Add an image
button a new image can be selected.
Click the button Add post
and go back to the Profile
screen by clicking the back button on the top left corner. The new user image is reflected on the screen as well as the Firestore.
The Profile
screen implemented is from one of the templates from Crowdbotics’ react-native collection. We use UI Kitten for our latest template libraries. You can modify the screen further or add another component that takes care of counting likes or comments. Find more about how to create custom screens like this from our open source project here.
You can also find the source code from this tutorial at this Github repo.