Create a Booking App UIs Using React-Native — Layouting UIs (Part 3)

Upscalix
11 min readOct 13, 2023

--

We’ve talked about project initiation in part 1 and how to create stack navigator & bottom tab navigator in part 2. On the third part of this article, we will start the layout processes based on the existing design.

The design that we will implement is as follows.

(left: login, right: home)
(left: search, right: detail)
  1. Custom Input Component

Because we will use react-hook-form, we will first create a custom input component. This custom input component makes it easier to use input fields without having to repeatedly configure the react-hook-form controller.

Here is the custom input component code that we will use. Save it in a new folder named components and name the file Input.js.

import React from 'react';
import {TextInput} from 'react-native';

import {useController} from 'react-hook-form';

function Input({
autoCorrect,
autoComplete,
name,
control,
placeholder,
testID,
style,
placeholderTextColor,
secureTextEntry,
required
}) {
const {field} = useController({
name,
control,
defaultValue: '',
rules: {
required
}
});

return (
<TextInput
autoCorrect={autoCorrect}
autoComplete={autoComplete}
testID={testID}
value={field.value}
onChangeText={field.onChange}
placeholder={placeholder}
style={style}
placeholderTextColor={placeholderTextColor}
secureTextEntry={secureTextEntry}
/>
)
}

export default Input;

2. Remove Header in Stack and Bottom Tabs Navigator

Since we don’t need the default header from the navigator, we will set the header to be empty.

To do this, in the declaration of <Stack.Navigator> in the “App.tsx” file, add the screenOptions prop as follows, and this also applies to the Bottom Tabs Navigator in screens/BottomTabs.js.

<Stack.Navigator
screenOptions={{
header: () => null,
}}
<ReactNavigationBottomTabs.Navigator
screenOptions={{
header: () => null,
}}

3. Login Page

Based on the existing design, there is 1 text for the title, 2 input fields and one button with text inside it. Let’s modify the code in “Login.js” to create a UI skeleton.

import React from 'react';
import {SafeAreaView, Text, TouchableOpacity, View} from 'react-native';

import Input from '../components/Input';

const Login = () => {
return (
<SafeAreaView>
<View>
<Text></Text>

<View>
<Input />
<Input />
</View>

<TouchableOpacity>
<Text></Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};

export default Login;

Then, from this UI skeleton, we will add styles to it, matching the design and the styles we created.

import React from 'react';
import {StyleSheet, SafeAreaView, Text, TouchableOpacity, View} from 'react-native';

import Input from '../components/Input';

const Login = () => {
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<Text style={styles.title}>Tour App</Text>

<View style={styles.textInputsContainer}>
<Input
name='username'
autocomplete='username'
autoCorrect={false}
placeholder='Username'
style={styles.textInput}
placeholderTextColor='darkgray'
/>

<Input
name='password'
autocomplete='password'
autoCorrect={false}
placeholder='Password'
style={styles.textInput}
placeholderTextColor='darkgray'
secureTextEntry
/>
</View>

<TouchableOpacity style={styles.button}>
<Text style={styles.buttonLabel}>Login</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
container: {
padding: 20,
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
color: 'black',
fontSize: 36,
fontWeight: 'bold',
},
textInputsContainer: {
width: '100%',
},
textInput: {
backgroundColor: 'white',
color: 'black',
width: '100%',
padding: 10,
height: 40,
borderRadius: 10,
marginTop: 20,
},
button: {
width: '100%',
padding: 15,
borderRadius: 10,
alignItems: 'center',
},
buttonLabel: {
color: 'white',
fontSize: 28,
fontWeight: 'bold',
},
});

The login page UI has been successfully created. Now, let’s add form functionality and import useForm.

import {useForm} from 'react-hook-form'; 

// Declare the hooks.

const {
control,
formState: {isValid},
} = useForm();

Add control from the hooks form and set the required attribute for input values to true.

<Input
control={control}
name='username'
autocomplete='username'
autoCorrect={false}
placeholder='Username'
style={styles.textInput}
placeholderTextColor='darkgray'
required={true}
/>

<Input
control={control}
name='password'
autocomplete='password'
autoCorrect={false}
placeholder='Password'
style={styles.textInput}
placeholderTextColor='darkgray'
secureTextEntry
required={true}
/>

Lastly, add the disabled property to control the button’sbutton’s activation only when the form is complete and valid. The onPress should contain a custom function submitLogin, where submitLogin includes the command to navigate to the BottomTabs page.

const submitLogin = () => {
props.navigation.replace('BottomTabs');
}
<TouchableOpacity
onPress={submitLogin}
disabled={!isValid}
style={[
styles.button,
{
backgroundColor: isValid ? 'mediumseagreen' : 'dimgray'
}
]}
>
<Text style={styles.buttonLabel}>Login</Text>
</TouchableOpacity>

Remember to change const Login = () => { to const Login = props => { because we will retrieve props from the login page.

4. Home Page

We will implement a list and react-native-swiper on this page to display slides.

We will work on it section by section, starting with the header, then the slider and finally the category list section.

4a. Home Header

The first part is the header of the Home page, which consists of a text input, a search button and a text title.

Here, we modify the code in Home.js to create a UI skeleton like this.

import React from 'react';
import {SafeAreaView, TextInput, TouchableOpacity, View} from 'react-native';

import FontAwesome from 'react-native-vector-icons/FontAwesome';

const Home = () => {
return (
<SafeAreaView>
<View>
<View>
<TextInput />

<TouchableOpacity>
<FontAwesome />
</TouchableOpacity>
</View>

<View>
<View>
<Text></Text>
</View>
</View>
</View>
</SafeAreaView>
)
}

export default Home;

Afterwards, we add styles to this code skeleton to match the existing design.

<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<View style={styles.searchSectionContainer}>
<TextInput
placeholder='Search Destination'
placeholderTextColor='darkgray'
style={styles.searchTextInput}
/>

<TouchableOpacity
activeOpacity={0.6}
style={styles.searchSubmitButton}
>
<FontAwesome name='search' color='white' size={20} />
</TouchableOpacity>
</View>

<View style={styles.mainContentContainer}>
<View style={styles.welcomeSectionContainer}>
<Text style={styles.welcomeSectionLabel}>Welcome to Tour App!</Text>
</View>
</View>
</View>
</SafeAreaView>
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: 'mediumseagreen',
},
container: {
backgroundColor: 'white',
flex: 1,
},
searchSectionContainer: {
backgroundColor: 'mediumseagreen',
flexDirection: 'row',
padding: 15,
},
searchTextInput: {
backgroundColor: 'white',
color: 'black',
flex: 1,
padding: 10,
borderRadius: 10,
},
searchSubmitButton: {
backgroundColor: 'goldenrod',
marginLeft: 15,
width: 40,
height: 40,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
mainContentContainer: {
flex: 1,
},
welcomeSectionContainer: {
backgroundColor: 'mediumseagreen',
padding: 20,
alignItems: 'center',
justifyContent: 'center',
},
welcomeSectionLabel: {
color: 'white',
fontSize: 20,
fontWeight: 'bold',
},
});

Then, it will look like this and the Home Header section is complete.

4b. Home Slider

Now, let’s move on to the slider. We will use react-native-swiper. First, let’s create some dummy data that we can later display in the slider.

import React from 'react';
import {SafeAreaView, TextInput, TouchableOpacity, View} from 'react-native';

import FontAwesome from 'react-native-vector-icons/FontAwesome';

const Home = () => {
return (
<SafeAreaView>
<View>
<View>
<TextInput />

<TouchableOpacity>
<FontAwesome />
</TouchableOpacity>
</View>

<View>
<View>
<Text></Text>
</View>
</View>
</View>
</SafeAreaView>
)
}

export default Home;

Then, we import the library using import Swiper from ‘’react-native-swiper’’ and declare the UI slider and its styles below the title section. We place it inside a ScrollView because the slider is part of the vertically scrollable content.

<ScrollView
contentContainerStyle={styles.scrollViewContentContainer}
style={styles.scrollViewContainer}
>
<View style={styles.sliderContainer}>
<Swiper
showPagination={true}
height={200}
dotColor='gray'
activeDotColor='goldenrod'
>
{sliderData.map(slider => {
return (
<View
key={slider.id}
>
<Image
source={{
uri: slider.url,
}}
style={styles.sliderImage}
/>
</View>
)
})}
</Swiper>
</View>
</ScrollView>
scrollViewContentContainer: {
flexGrow: 1,
},
scrollViewContainer: {
flex: 1,
},
sliderContainer: {
height: 250,
},
sliderImage: {
height: 250,
width: '100%'
},

So, we have successfully displayed the slider with the dummy data that we prepared.

4c. Categories: Home

We prepare dummy data to display the category list section as follows.

const categories = [
{
id: 0,
name: 'Adventure',
items: [
{
id: 0,
name: 'Mount Ijen',
uri: 'https://images.unsplash.com/photo-1555058170-94d5f5016a2c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
},
{
id: 1,
name: 'Mount Bromo',
uri: 'https://images.unsplash.com/photo-1602154663343-89fe0bf541ab?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1631&q=80',
},
],
},
{
id: 1,
name: 'Historical',
items: [
{
id: 0,
name: 'Museum Fatahillah',
uri: 'https://images.unsplash.com/photo-1642064816139-36f0f72192b0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2940&q=80',
},
{
id: 1,
name: 'National Monumemnt',
uri: 'https://images.unsplash.com/photo-1611637405506-e6e6ca710362?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2874&q=80',
},
],
},
];

Following the declaration of UI and its styles, we display text that says “By Type,” and then we show a list with category names. Each category will display a horizontal list of items within it.

<Text style={styles.byCategoryLabel}>By Type</Text>

{categories.map(category => {
return (
<View key={category.id} style={styles.categoryContainer}>
<Text style={styles.categoryNameLabel}>
{category.name}
</Text>

<ScrollView
horizontal
contentContainerStyle={
styles.categoryScrollViewContentContainer
}>
{category.items.map(categoryItem => {
return (
<TouchableOpacity
key={categoryItem.id}
activeOpacity={0.6}
style={styles.categoryItemButton}
onPress={() =>
props.navigation.navigate('DestinationDetail')
}>
<Image
style={styles.categoryItemImage}
source={{uri: categoryItem.uri}}
/>

<Text style={styles.categoryItemName}>
{categoryItem.name}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
</View>
);
})}
byCategoryLabel: {
color: 'black',
fontSize: 20,
fontWeight: 'bold',
padding: 20,
},
categoryContainer: {
backgroundColor: 'rgb(60, 60, 60)',
paddingBottom: 20,
marginBottom: 10,
},
categoryNameLabel: {
color: 'white',
fontSize: 20,
fontWeight: 'bold',
padding: 20,
},
categoryScrollViewContentContainer: {
paddingRight: 20,
},
categoryItemButton: {
marginLeft: 20,
},
categoryItemImage: {
height: 200,
width: 150,
borderRadius: 10,
},
categoryItemName: {
color: 'white',
textAlign: 'center',
marginTop: 10,
fontWeight: 'bold',
},

We have successfully displayed the category list and the list of items for each category. Notice that on each item’sitem’s button, there is onPress={() => props.navigation.navigate(‘’DestinationDetail’’)}. We set it up this way so that when users press the item button, it will take them to the DestinationDetail page. As a note, the props are taken from the Home page by declaring Home like this: const Home = props => {…}.

4d. Search Home

The final part of the user interface for the Home page is the implementation of the search feature. This search feature is still part of the Home page, but its display is conditional. If the input is active, we will display the Home content, and if the information is not functional, we will show the search section.

Let’s start by creating two new states, one to store the search input value.

const [search, setSearch] = useState('');
const [isSearchInputFocused, setIsSearchInputFocused] = useState(false);

In the declaration of the UI input, we add the following props:

<TextInput
placeholder="Search Destination"
placeholderTextColor="darkgray"
style={styles.searchTextInput}
onFocus={() => setIsSearchInputFocused(true)}
onBlur={() => setIsSearchInputFocused(false)}
value={search}
onChangeText={value => setSearch(value)}
autoCorrect={false}
/>

We save the input’s focus state using callbacks onFocus and onBlur, and then we use the onChangeText callback to store the latest input value.

Next, we move the main content from the View with the welcomeSectionContainer style up to the closing tag of ScrollView into a variable named MainContent.

const MainContent = (
<>
<View style={styles.welcomeSectionContainer}>
<Text style={styles.welcomeSectionLabel}>Welcome Tour App</Text>
</View>

<ScrollView
contentContainerStyle={styles.scrollViewContentContainer}
style={styles.scrollViewContainer}>
<View style={styles.sliderContainer}>
<Swiper
showsPagination={true}
height={200}
dotColor="gray"
activeDotColor="goldenrod">
{sliderData.map(slider => {
return (
<View key={slider.id}>
<Image
source={{
uri: slider.uri,
}}
style={styles.sliderImage}
/>
</View>
);
})}
</Swiper>
</View>

<Text style={styles.byCategoryLabel}>My Type</Text>

{categories.map(category => {
return (
<View key={category.id} style={styles.categoryContainer}>
<Text style={styles.categoryNameLabel}>{category.name}</Text>

<ScrollView
horizontal={true}
contentContainerStyle={
styles.categoryScrollViewContentContainer
}>
{category.items.map(categoryItem => {
return (
<TouchableOpacity
key={categoryItem.id}
activeOpacity={0.6}
style={styles.categoryItemButton}
onPress={() =>
props.navigation.navigate('DestinationDetail')
}>
<Image
style={styles.categoryItemImage}
source={{uri: categoryItem.uri}}
/>

<Text style={styles.categoryItemName}>
{categoryItem.name}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
</View>
);
})}
</ScrollView>
</>
);

We create variable dummy destinations containing dummy search options.

const destinations = [
'Mount Ijen',
'Mount Kerinci',
'Museum Fatahillah',
'National Monument',
];

We also create a new variable called SearchSection, and the UI and code for SearchSection will look like the following:

const SearchSection = (
<View style={styles.searchPageContainer}>
{destinations
.filter(item => item.toLowerCase().includes(search.toLowerCase()))
.map(item => {
return (
<TouchableOpacity
activeOpacity={0.6}
onPress={() => props.navigation.navigate('DestinationDetail')}
style={styles.searchItemButton}>
<Text>{item}</Text>
</TouchableOpacity>
);
})}
</View>
);
searchPageContainer: {
position: 'absolute',
top: 0,
right: 0,
left: 0,
bottom: 0,
},
searchItemButton: {
borderBottomWidth: 1,
borderColor: 'gray',
padding: 20,
},

Once we’ve created the MainContent and SearchSection separately in their respective variables, it becomes easier to describe their conditional rendering.

<View style={styles.mainContentContainer}>
{isSearchInputFocused ? SearchSection : MainContent}
</View>
mainContentContainer: {
flex: 1,
},

Up to this point, we have successfully created the Home page, which includes the header, slider, categories and search sections. Next, we will make the DestinationDetail page.

5. DestinationDetail Page

This is the last page we will create in this article. Here, we will display a slider that will show multiple photos, names, prices, descriptions and the location of the destination.

We will gradually create the layout for this page, starting with the header, then the slider, followed by the metadata along with the map and finally the order button.

Open the DestinationDetail.js file.

5a. Destination Detail Header

Here is the code we will use to create the DestinationDetail page header.

const DestinationDetail = props => {
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.headerContainer}>
<TouchableOpacity
onPress={props.navigation.goBack}
style={styles.backButton}>
<FontAwesome name="chevron-left" size={24} color="white" />
</TouchableOpacity>

<Text style={styles.headerText}>Destination Detail</Text>
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
safeArea: {
backgroundColor: 'mediumseagreen',
flex: 1,
},
headerContainer: {
alignItems: 'center',
flexDirection: 'row',
padding: 20,
},
backButton: {
alignItems: 'center',
justifyContent: 'center',
},
headerText: {
color: 'white',
fontSize: 20,
fontWeight: 'bold',
marginLeft: 20,
},
});

5b. Destination Detail Slider

We will use the same approach as the header on the Home page for the slider section.

Let’s start by creating some dummy data, for example, images that will be displayed.

const sliderData = [
{
id: 0,
uri: 'https://images.unsplash.com/photo-1571366343168-631c5bcca7a4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1471&q=80',
},
{
id: 1,
uri: 'https://images.unsplash.com/photo-1684141419225-d56a858465b3?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
},
];

And here is the UI code for our slider. We will display it below the previously created header and place it within a new ScrollView.

<ScrollView style={styles.scrollView}>
<View style={styles.sliderContainer}>
<Swiper
showsPagination
activeDotColor="mediumseagreen"
dotColor="gray">
{sliderData.map(item => {
return (
<Image
key={item.id}
source={{
uri: item.uri,
}}
style={styles.sliderImage}
/>
);
})}
</Swiper>
</View>
</ScrollView>
scrollView: {
backgroundColor: 'white',
flex: 1,
},
sliderContainer: {
height: 250,
},
sliderImage: {
height: 250,
width: '100%'
},

5c. Destination Detail Metadata

In this section, we will display the destination’s name, price, description and location, which requires 3 text elements and 1 MapView.

We import the MapView from ‘’react-native-maps’’ and create a variable, e.g. const price = 55; for the price example. Here is the complete code for layouting this section.

<View style={styles.destinationLabelContainer}>
<Text style={styles.destinationLabel}>Padar Island</Text>

<Text style={styles.destinationPriceLabel}>${price} / pax</Text>
</View>

<Text style={styles.descriptionLabel}>
Experience the enchanting beauty of Padar Island's lush landscape scenery
</Text>

<View style={styles.mapContainer}>
<MapView
scrollEnabled={false}
rotateEnabled={false}
zoomEnabled={false}
initialRegion={{
latitude: -6.9903587,
longitude: 110.4230505,
latitudeDelta: 0.005,
longitudeDelta: 0.005,
}}
style={styles.map}
/>
</View>
destinationLabelContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
padding: 20,
},
destinationLabel: {
fontSize: 20,
fontWeight: 'bold',
},
destinationPriceLabel: {
fontSize: 20,
color: 'mediumseagreen',
fontWeight: 'bold',
},
descriptionLabel: {
paddingHorizontal: 20,
},
mapContainer: {
marginHorizontal: 20,
marginTop: 20,
},
map: {
height: 200,
width: '100%',
},

5d. Order Button Section Destination Detail

The last part is the order button section, which is quite simple. We create a static View that will always appear below and outside the ScrollView, containing a single button.

Here is the code for its appearance. Place it below and outside the ScrollView.

<View style={styles.orderNowButtonContainer}>
<TouchableOpacity
activeOpacity={0.6}
style={styles.orderNowButton}
onPress={() =>
props.navigation.navigate('PersonalInformation', {price})
}>
<Text style={styles.orderNowButtonLabel}>Order Now</Text>
</TouchableOpacity>
</View>
orderNowButtonContainer: {
padding: 20,
backgroundColor: 'mediumseagreen',
alignItems: 'flex-end',
},
orderNowButton: {
backgroundColor: 'goldenrod',
paddingVertical: 10,
paddingHorizontal: 15,
borderRadius: 10,
},
orderNowButtonLabel: {
fontSize: 20,
color: 'white',
fontWeight: 'bold',
},

Notice that on the “Order Now” button, an onPress navigates the user to the PersonalInformation page, followed by passing the data parameter price from this page to the destination page.

The layout of the DestinationDetail page is complete. We will continue with the following article for the design of the PersonalInformation, SuccessOrder and OrderList pages.

Continue to part 4.

Interested in realising your dream app to help your business and your target market? Contact Upscalix now.

This article is written by Upscalix Senior Frontend and Mobile Developer, Nova.

--

--