Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
In most advanced cities, people no longer need to physically visit restaurants to order the food they desire. The internet enables them to order food online and have it delivered promptly to their preferred location, whether it's their office or home. With the food ordering app, you can browse through the list of available food menus, place an order, and have it delivered to you without leaving your comfort zone.
This tutorial will walk you through building a food ordering mobile app using React Native and Strapi. The app will feature functionalities for users to browse through a selection of foods, add them to their cart, and manage user authentication and profiles. By the end of this guide, you will have created a mobile food application similar to the one shown in the following image:
Before diving into this tutorial, ensure you have the following prerequisites:
Let’s set up the Strapi backend for the application. If you already have a Strapi project, you can skip to the next section. Otherwise, open your terminal and create a new Strapi project using the command below.
npx create-strapi-app@latest my-project --quickstart
After the installation is complete, the Strapi server will start and open in your browser. Register and log in to your dashboard as shown in the image below.
Next, we'll create collection types for our application, specifically: RegisterUsers
, Product
, and Cart
. Here's how to set them up:
Create New Collection Type.
RegisterUsers
and click Continue``.
Refer to the image below for guidance.Now let’s structure the registerusers
collection type by adding the following fields to it:
fullname
: type→textemail
: type→Emailpassword
: type→textcity
: type→textstate
: type→textRepeat the above steps to create the following collection types:
Product Collection:
productname
: type→textprice: type
→textimage: type
→mediaCart Collection: Create a Cart collection type containing the following fields.
productid
: type→textuserid
: type→textAfter creating the collection types, navigate to Settings → Role → Public
and select all operations for each collection type to make them accessible via the API endpoint, as shown in the image below.
Now, navigate to the content manager, click on the "Product
" collection type, and add as many products as you'd like.
With the Strapi backend ready, our next step is to build the mobile application interface. The complete source code for this mobile app is available on GitHub.
To get started, let’s clone the mobile application to your local computer. Open your terminal and execute the following command:
git clone https://github.com/popoolatopzy/StrapiFoodApp
After cloning the application, let’s install all the necessary dependencies. To install the dependencies run the commands below on your terminal.
cd StrapiFoodApp
npm install
The above command installs important dependencies like: React Navigation: This is a popular and widely used library for managing navigation and routing in React Native applications. It provides a structured approach to handling the app's navigation flow, encompassing features such as screen creation, stack navigation, tab navigation, and drawer navigation. React Native Async Storage: A package that provides a straightforward, asynchronous key-value storage system. It is used for caching data, saving user preferences, and restoring session information.
Open the cloned app in your code editor. The app includes four primary screens located in the "components` folder. We'll explore these components for a better understanding of their functionality.
Authentication Screens
Before users can access the food app, they must first register and log in to the application. To create the registration component, navigate to the ‘components
' folder, create a ‘RegisterScreen.js
' file, and add the following code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import React, { useState } from "react";
import { View, Text, TextInput, Button, StyleSheet } from "react-native";
export default function AboutScreen({ navigation }) {
const [email, setEmail] = useState("");
const [fullName, setFullName] = useState("");
const [password, setPassword] = useState("");
const [country, setCountry] = useState("");
const [state, setState] = useState("");
const handleRegister = async () => {
try {
// Define the registration data
const registrationData = {
fullname: fullName,
email: email,
password: password,
city: country,
state: state,
};
// Send a POST request to the registration endpoint
const response = await fetch("http://localhost:1337/api/registerusers", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ data: registrationData }),
});
console.log(response);
if (response.ok) {
// Registration successful, navigate to the login screen
navigation.navigate("Login");
} else {
// Handle the case where registration is unsuccessful
console.error("Registration failed");
}
} catch (error) {
console.error("Error during registration:", error);
// Handle the error, e.g., display an error message
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Registration</Text>
<TextInput
style={styles.input}
placeholder="Email"
onChangeText={(text) => setEmail(text)}
value={email}
/>
<TextInput
style={styles.input}
placeholder="Full Name"
onChangeText={(text) => setFullName(text)}
value={fullName}
/>
<TextInput
style={styles.input}
placeholder="Password"
onChangeText={(text) => setPassword(text)}
value={password}
secureTextEntry={true}
/>
<TextInput
style={styles.input}
placeholder="Country"
onChangeText={(text) => setCountry(text)}
value={country}
/>
<TextInput
style={styles.input}
placeholder="State"
onChangeText={(text) => setState(text)}
value={state}
/>
<Button title="Register" onPress={handleRegister} />
</View>
);
}
const styles = StyleSheet.create({
});
From the code above, we created a registration form and then sent the user details to our Strapi "registeruser
" endpoint to store the user information. After successful registration, the user is redirected to the login screen.
Next, create a LoginScreen.js
file inside the component
folder. This component is used by the registered user to log into their account using an email
and password
. Add the following code the file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import React, { useState } from "react";
import { View, Text, TextInput, Button, StyleSheet } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function LoginScreen({ navigation }) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
try {
// Construct the URL with query parameters
const baseUrl = "http://localhost:1337/api/registerusers";
const queryParams = `?email=${email}&password=${password}`;
const url = baseUrl + queryParams;
// Send a GET request with the constructed URL
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data && data.data && data.data.length > 0) {
// Assuming the response contains an array
for (let i = 0; i < data.data.length; i++) {
const user = data.data[i];
const userId = user.id; // Get the user ID
const email2 = user.attributes.email;
const fullname = user.attributes.fullname;
const password2 = user.attributes.password;
const info = user.attributes;
if (email==email2 && password==password2) {
const keyValues = [
["userId", userId.toString()],
["email", email2.toString()],
["fullname", fullname.toString()],
];
// console.log(fullname)
await AsyncStorage.multiSet(keyValues);
navigation.navigate("Main");
}
}
} else {
}
} catch (error) {
console.error("Error during registration:", error);
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Login</Text>
<TextInput
style={styles.input}
placeholder="Email"
onChangeText={(text) => setEmail(text)}
value={email}
/>
<TextInput
style={styles.input}
placeholder="Password"
onChangeText={(text) => setPassword(text)}
value={password}
secureTextEntry={true}
/>
<Button title="Login" onPress={handleLogin} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
title: {
fontSize: 24,
marginBottom: 20,
},
input: {
width: 300,
height: 40,
borderColor: "gray",
borderWidth: 1,
borderRadius: 5,
paddingHorizontal: 10,
marginBottom: 15,
},
});
From the code above, we created a login form and checked if the user email and password is correct by making a request to the registeruser
endpoint to fetch the user information. After successful registration, the user is redirected to the home screen.
Food home screen All the lists of available products are displayed in the home screen, where users can browse through products, view them and add products to cart. All the products are fetched from the Strapi product endpoint.
Create a HomeScreenn.js
file and add the following code into it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import React, { useState, useEffect } from "react";
import { View, Text, Button, FlatList, StyleSheet, Image, Platform } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function HomeScreen({ navigation }) {
const [products, setProducts] = useState([]);
useEffect(() => {
async function fetchProducts() {
try {
const response = await fetch("http://localhost:1337/api/products?populate=*");
const data = await response.json();
if (data && data.data) {
const userId = await AsyncStorage.getItem("userId");
setProducts(data.data);
}
} catch (error) {
console.error("Error fetching products:", error);
}
}
fetchProducts();
}, []);
const renderProducts = ({ item }) => {
const bb = "http://localhost:1337" + item.attributes.image.data.attributes.formats.small.url;
return (
<View style={styles.recipeItem} key={item.id}>
<Image source={{ uri: bb }} style={styles.recipeImage} />
<Text style={styles.recipePrice}>Price: ${item.attributes.price}</Text>
<Button
title={item.attributes.productname}
onPress={() => navigation.navigate("View", { recipe1: item.id})}
/>
</View>
);
};
return (
<View style={styles.container}>
<FlatList
data={products}
renderItem={renderProducts}
keyExtractor={(item) => item.id.toString()}
numColumns={2}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 20,
},
title: {
fontSize: 24,
marginBottom: 20,
},
recipeItem: {
width: 150,
height: 200,
marginBottom: 20,
marginLeft: 10,
marginRight: 10,
borderRadius: 10,
backgroundColor: "white",
...Platform.select({
ios: {
shadowColor: "black",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 2,
},
android: {
elevation: 2,
},
}),
alignItems: "center",
justifyContent: "center",
},
recipeImage: {
width: 100,
height: 100,
borderRadius: 100,
},
recipeName: {
fontSize: 18,
fontWeight: "bold",
marginBottom: 5,
},
recipePrice: {
fontSize: 16,
},
});
From the code above, we make a GET request to the Strapi product endpoint to retrieve and display the list of all available food items. Users can click on a product to view more details about the food.
Now, we will create a Product Preview screen that shows more details about the food the user wants to order, and they can add the food to their cart list. In the components folder, create ViewScreen
and add the following code to it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import React, { useEffect, useState } from "react";
import { View, Text, Image, StyleSheet, Button, TouchableOpacity } from "react-native";
import { FontAwesome } from "@expo/vector-icons";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function ViewScreen({ route, navigation }) {
const { recipe1 } = route.params;
const [recipe, setRecipe] = useState(null);
useEffect(() => {
async function fetchRecipe() {
try {
const response = await fetch(`http://localhost:1337/api/products/${recipe1}?populate=*`);
const data = await response.json();
if (data && data.data) {
setRecipe(data.data.attributes);
}
} catch (error) {
console.error("Error fetching recipe:", error);
}
}
fetchRecipe();
}, [recipe1]);
// Function to handle adding the recipe to the cart
const handleAddToCart = async () => {
console.log(recipe1)
if (recipe) {
const userId = await AsyncStorage.getItem("userId");
const payload = {
userid: userId,
productid: recipe1, // Adjust this as needed based on your data structure
};
try {
const response = await fetch("http://localhost:1337/api/carts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ data: payload }),
});
if (response.ok) {
alert(`Added "${recipe.productname}" to the cart`);
navigation.navigate("Main");
} else {
console.error("Error adding to cart. Please try again.");
}
} catch (error) {
console.error("Error adding to cart:", error);
}
}
};
return (
<View style={styles.container}>
{recipe ? (
<React.Fragment>
<Image source={{ uri: "http://localhost:1337" + recipe.image.data.attributes.formats.small.url }} style={styles.recipeImage} />
<Text style={styles.recipeName}>{recipe.productname}</Text>
<Text style={styles.recipePrice}>Price: ${recipe.price}</Text>
</React.Fragment>
) : (
<Text>Loading...</Text>
)}
{/* Add to Cart Button */}
<TouchableOpacity style={styles.addToCartButton} onPress={handleAddToCart}>
<FontAwesome name="shopping-cart" size={24} color="white" />
<Text style={styles.addToCartButtonText}>Add to Cart</Text>
</TouchableOpacity>
<Button title="Go Back" onPress={() => navigation.goBack()} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
padding: 20,
marginTop: 100,
},
recipeImage: {
width: 200,
height: 150,
marginBottom: 10,
borderRadius: 20,
},
recipeName: {
fontSize: 18,
fontWeight: "bold",
},
recipePrice: {
fontSize: 16,
marginBottom: 10,
},
addToCartButton: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#007bff",
padding: 10,
borderRadius: 5,
},
addToCartButtonText: {
color: "white",
fontSize: 18,
marginLeft: 10,
},
});
In the code above, we retrieved the product ID of the preview food from the screen URL using React Router and made a request to the Strapi product endpoint to fetch details about the product, passing the product ID along with the request.
Also, the handleAddToCart()
function is used to add a new product to the user's cart list by making a POST request to the cart endpoint.
Now, let's create a Cart Screen where all the foods the users have added to the cart will be displayed. To do that, create a ‘CartScreen.js
' file in the 'component' folder and add the following code to it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import React, { useState, useEffect } from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function CartScreen({ navigation }) {
const [cartItems, setCartItems] = useState([]);
const [cart, setCart] = useState([]);
const [user, setUser] = useState();
const [cartList, setCartList] = useState([]);
const fetchCartData = async () => {
try {
const response = await fetch("http://localhost:1337/api/carts");
const data = await response.json();
if (data && data.data) {
const userId = await AsyncStorage.getItem("userId");
setUser(userId);
setCart(data.data);
}
} catch (error) {
console.error("Error fetching cart items:", error);
}
};
const fetchCartItems = async () => {
try {
const response = await fetch("http://localhost:1337/api/products?populate=*");
const data = await response.json();
if (data && data.data) {
setCartItems(data.data);
}
} catch (error) {
console.error("Error fetching cart items:", error);
}
};
useEffect(() => {
const fetchData = async () => {
await fetchCartData();
await fetchCartItems();
const updatedCartList = [];
for (let i = 0; i < cart.length; i++) {
if (cart[i].attributes.userid == user) {
for (let a = 0; a < cartItems.length; a++) {
if (cart[i].attributes.productid == cartItems[a].id) {
updatedCartList.push(cartItems[a]);
}
}
}
}
setCartList(updatedCartList);
};
fetchData();
}, [cart]);
const calculateTotalPrice = () => {
let totalPrice = 0;
for (const item of cartList) {
totalPrice += parseFloat(item.attributes.price);
}
return totalPrice.toFixed(2);
};
return (
<View style={styles.container}>
<Text style={styles.title}>Cart</Text>
{cartList.map((item) => (
<View style={styles.cartItem} key={item.id}>
<View style={styles.horizontalContainer}>
<Image source={{ uri: "http://localhost:1337" + item.attributes.image.data.attributes.formats.small.url }} style={styles.itemImage} />
<View style={styles.productDetails}>
<Text style={styles.itemName}>{item.attributes.productname}</Text>
<Text style={styles.itemPrice}>Price: ${item.attributes.price}</Text>
</View>
</View>
</View>
))}
<Text style={styles.totalPrice}>Total Price: ${calculateTotalPrice()}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
title: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 20,
},
cartItem: {
backgroundColor: "white",
borderRadius: 5,
padding: 7,
marginBottom: 10,
},
horizontalContainer: {
flexDirection: "row",
},
itemImage: {
width: 100,
height: 100,
borderRadius: 5,
},
productDetails: {
marginLeft: 10,
},
itemName: {
fontSize: 18,
fontWeight: "bold",
},
itemPrice: {
fontSize: 16,
},
totalPrice: {
fontSize: 18,
fontWeight: "bold",
marginTop: 10,
},
});
In the code above, we fetch the list of all the food that a user has added to the cart and then calculate and display the total price for all the food in the cart.
To create a profile screen where details about the logged-in user are displayed with a logout button, inside the component folder, create a ProfileScreen.js
file and add the following code to it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React, { useState, useEffect } from "react";
import { View, Text, Image, Button, StyleSheet } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function ProfileScreen({ navigation }) {
const [username, setusername] = useState();
const [email, setemail] = useState();
useEffect(() => {
async function info() {
try {
setusername(await AsyncStorage.getItem("fullname"));
setemail(await AsyncStorage.getItem("email"));
} catch (error) {
console.error("Error fetching cart items:", error);
}
}
info()
}, []);
return (
<View style={styles.container}>
<Image source={require("../assets/splash2.png")} style={styles.profileImage} />
<Text style={styles.name}>{username}</Text>
<Text style={styles.email}>{email}</Text>
{/* Add more user details as needed */}
<Button title="Edit Profile" onPress={() => navigation.navigate("EditProfile")} />
<Button title="Logout" onPress={() => navigation.navigate("Login")} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
},
profileImage: {
width: 150,
height: 150,
borderRadius: 75, // Half of the width and height to make it circular
marginBottom: 20,
},
name: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 10,
},
email: {
fontSize: 16,
marginBottom: 20,
},
});
In the code above, user information is retrieved from local storage and displayed on the profile screen, while the ‘log out
' button ends the user session and redirects them to the login screen.
Now that we have created all the necessary components, let's connect all the components together using React Native Navigation. Open the 'app.js'
file from the project's root folder, and add the following code to it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import * as React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createStackNavigator } from "@react-navigation/stack";
import Icon from "react-native-vector-icons/FontAwesome";
import HomeScreen from "./components/HomeScreen";
import ProfileScreen from "./components/ProfileScreen";
import ViewScreen from "./components/ViewScreen";
import CartScreen from "./components/CartScreen";
import LoginScreen from "./components/LoginScreen";
import RegisterScreen from "./components/RegisterScreen";
import SplashScreen from "./components/SplashScreen";
const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();
function MainTabNavigator() {
return (
<Tab.Navigator>
<Tab.Screen
name="Food App"
component={HomeScreen}
options={{
tabBarIcon: ({ color, size }) => (
<Icon name="home" size={size} color={color} />
),
}}
/>
<Tab.Screen
name="Cart"
component={CartScreen}
options={{
tabBarIcon: ({ color, size }) => (
<Icon name="shopping-cart" size={size} color={color} />
),
}}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{
tabBarIcon: ({ color, size }) => (
<Icon name="user" size={size} color={color} />
),
}}
/>
</Tab.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<Stack.Navigator headerMode="none">
<Stack.Screen name="Splash" component={SplashScreen} />
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
<Stack.Screen name="Main" component={MainTabNavigator} />
<Stack.Screen name="View" component={ViewScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
Next, let’s start the application by running the command below on your terminal.
npx expo start
After starting the application server, follow the prompts to get the application running on your simulator. The application should be up and running, as shown in the image below.
Congratulations, you have successfully created a mobile food application using React Native and Strapi!
Throughout this tutorial, we have successfully walked through the process of creating a food ordering application utilizing React Native and Strapi. For further reference and exploration:
Feel free to dive into these resources for a deeper understanding and to customize your application.