Blog Application with React Native
Mobile application development with React Native
Table of contents
Introduction
For the final quest of this campaign, we will be creating a blog mobile application where you will be able to write your personal blogs and diaries. These blogs will be created and stored locally on your device. You may recall that previously we used static data to populate the application. However, we will now learn how to store text data on local devices. This is made possible with Expo’s AsyncStorage.
For this quest, we will be creating a two page application. The first page will be where we show a concise preview of all of the blogs we have created and the next page will be where we can create, edit and delete our blogs.
Step 1: Project Setup
In this step, we will be setting up our environment to develop our blog application. Firstly, open any code editor of your choice. Choose any folder as your project root folder and open up your terminal.
Enter the following commands in your computer’s terminal.npx expo init blog-app cd blog-app npm install @react-navigation/native @react-navigation/native-stack npx expo install react-native-screens react-native-safe-area-context npm install @expo/vector-icons npm i @react-native-async-storage/async-storage npx expo start
This code sets up a new React Native project, blog-app using the Expo CLI tool. The first line initializes a new project using the expo init command, while the second line changes the current working directory to the newly created project.
The next set of commands installs several packages that are necessary for the project to run, including packages for navigation using @react-navigation/native and @react-navigation/native-stack. These packages are installed using the npm install command. Additionally, the npx expo install command is used to install other necessary dependencies, such as react-native-screens and react-native-safe-area-context.
Finally, the npm install command is used to install @expo/vector-icons[, a package that provides icons for use in the application. The last package we need is the @react-native-async-storage/async-storage package. This package will allow us to store local data onto our mobile application. Usually you may choose to store session token and other crucial information that can identify your users. You may also notice that we also used npm i instead of npm install. This is a shorthand for installing packages. You may use either when installing new packages for any JavaScript projects.
The last line, npx expo start, starts the Expo development server and launches the application in the emulator or on a connected device.
Step 2: File and Folder Management
Once we are done with downloading the dependencies, we will create some files and folders. First, in the blog-app folder, create a new folder called pages. In the pages folder, create two new files, IndexPage.js and BlogPage.js. These are the files we will be working with to display the two pages we will be creating in this quest.
As you may realise, Expo has installed and created the node_modules folder which contains all of the dependencies we need for this React Native project. The assets folder will be where you will be able to add in your own images. Any time when you may want to use any images from the assets folder, you should make the image path you are referencing relative to this folder.
Lastly, App.js will be the entry file that React Native will first interact with.
You may check your folder structure against the 'expected output' below.
Expected output
Step 3: Content Page and View Management
We will now set up our content page for this application. You may have recalled in the third quest, we used the App.js file to keep track of all of the files and pages that will be displayed on our application.
Replace all the template code in App.js with the code below.import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import IndexPage from './pages/IndexPage'; const Stack = createNativeStackNavigator();
In the code sample above, we have imported the dependencies that we installed in Step 1. These dependencies from React Navigation will be used to control what the user is seeing on their screen. For this quest, we will be using the Stack Navigation. The Stack Navigation is basic and easy to implement.
Replace the code within the return() statement of the App function with the code snippet as shown below.export default function App() { return ( <NavigationContainer> <Stack.Navigator> <Stack.Screen name="Index" component={IndexPage} options={{headerShown:false}}/> </Stack.Navigator> </NavigationContainer> ); }
To check if you correctly edited the code in App.js, you may refer to the 'working code' shown at the end of this step.
Let's understand our code now. The <NavigationContainer> component is the top-level component provided by the React Navigation library that manages the navigation state for your entire app.
Within the <NavigationContainer> component in line 9, a <Stack.Navigator> component is used to wrap the screens that will be managed by the Stack Navigation type. The <Stack.Navigator> component is responsible for controlling the stack-based navigation of your application.
The <Stack.Screen> component in line 11 is used to define a single screen within the stack-based navigation. Each <Stack.Screen> component must have a unique name prop, which is used to refer to that specific screen within the stack, and a component prop, the React component that will be rendered for that screen.
In this example, there is only one <Stack.Screen> component defined, with a name of "Index" and a component of IndexPage. This means that when the navigation stack is at this screen, the IndexPage component will be rendered.
Each time we would like to create a new page, each page needs to have their own <Stack.Screen> element. The two crucial props we will need to add in are the name and the component prop. In any project, each page should have a unique name. There should not be any screens with the same name prop. We can also use the option prop to adjust the different properties of the navigation container. In our case, we have opted for the header to not be shown on screen.
Working code
Step 4: Header Component
Now we will be learning how to add a simple header to our Index page. Save the image below as header.jpg and add it to the assets folder.
Open IndexPage.js and add the code below.import { View, Image, StyleSheet, Text } from "react-native"; const IndexPage = ({navigation}) => { return ( <View> <Image source={require('../assets/header.jpg')} style={styles.images} /> <Text style={styles.headerTitle}>Stackie's Blog</Text> </View> ); } const styles = StyleSheet.create({ images: { width: '100%', height: 200, resizeMode: 'cover', }, headerTitle: { fontSize: 40, fontWeight: 'bold', textAlign: 'left', marginTop: 10, color: 'white', textShadowColor: 'rgba(0, 0, 0, 0.9)', textShadowOffset: { width: -1, height: 1 }, textShadowRadius: 10, position: 'absolute', top: 140, left: 20, }, }); export default IndexPage;
The code above writes the structure for the header and also styles it. Please take note that you are to change the text in line 10 to your StackUp profile name to show that the blog belongs to you.
The IndexPage component returns a View component that contains an Image component and a Text component. The Image component uses a local image file located at "../assets/header.jpg" as its source. The styles for the Image component are defined in the styles constant object, which is created using the StyleSheet.create method. The styles specify the width, height, and resize mode of the image. You may notice that we are using "100%" for the width as we want the image to expand to the full width of the mobile screen. Since the physical size of the image will be different from screen to screen, in order to maintain the aspect ratio of the image, we will need to use the resizeMode: "cover" method to allow the image to maintain its aspect ratio.
The Text component displays the text "Stackie's Blog" and has styles defined in the styles constant object. The styles specify the font size, font weight, text alignment, color, and text shadow properties of the text. The text also has a specified position as dictated by the top and left properties.
Step 5: New Blog Button and Container
Now, let's create the button that we can use to create a new blog entry. We will use a component called <TouchableOpacity>. In React Native, this component allows us as developers to create custom buttons that will show feedback when clicked.
In line 1 of IndexPage.js, add “TouchableOpacity” behind the Text component in the import statement. It should look like the code below.import { View, Image, StyleSheet, Text, TouchableOpacity } from "react-native";
Bear in mind that for any component that you use, you will need to import them first. As a good practice, for any dependencies you need for a project, you should place the import statement at the top of your code.
After line 1, add the import statement below. This statement adds FontAwesome5 icons to our project.import { FontAwesome5 } from '@expo/vector-icons';
Next, after line 11 of the <Text> component, we will create a new <View> component that will hold the previews of our blogs and the button to create a new blog entry. Add the following code to after line 11 of IndexPage.js.<View style={styles.container}> {/* New Blog Entry Button */} <TouchableOpacity style={styles.button} onPress={() => navigation.navigate('BlogPage')} > <FontAwesome5 name="sticky-note" size={24} color="white" /> <Text style={styles.buttonText}>New Blog Entry</Text> </TouchableOpacity> </View>
The code above has a <View> component with styles defined in the styles object. The styles specify the layout and appearance of the container.
The button is created using a <TouchableOpacity> component, which is a touchable component that provides visual feedback when pressed. The styles for the button are defined in the styles object, and they specify the appearance of the button. The onPress prop specifies the action to be taken when the button is pressed. In this case, it navigates to the BlogPage route in the navigation stack.
The button also contains a <FontAwesome5> component, which displays an icon, and a <Text> component, which displays the text "New Blog Entry". The <name> prop specifies the name of the icon to display. The size prop specifies the size of the icon, and the color prop specifies the color of the icon. The styles for the text are defined in the styles object, and they specify the appearance of the text.
Now, we will move on to the styling of the button and the container. Add this code after line 45 of IndexPage.js.button: { backgroundColor: '#1E90FF', padding: 10, borderRadius: 10, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', }, buttonText: { color: 'white', fontSize: 20, fontWeight: 'bold', marginLeft: 10, }, container: { height: '100%', padding: 20, backgroundColor: '#031031', },
For this code, we will use flex to make the display flexible. Using flexDirection:"row", we can position the components in the View element as a row. We can also align them by using the alignItems and justifyContent properties.
To check if you edited IndexPage.js correctly, you can check your code against the 'working code' below.
Working code
Step 6: BlogPage.js Setup
Similar to what we did earlier for IndexPage, we will need to create a reference to the BlogPage.js file. In App.js, we will need to import the BlogPage.js file and add the <Stack.Screen> component within the <Stack.Navigator> components.
In your App.js file, add the following two lines of code after lines 3 and line 11 respectively.import BlogPage from './pages/BlogPage'; <Stack.Screen name="BlogPage" component={BlogPage} options={{ headerShown: false }} />
Your code below should be similar to the 'working code' below.
Working code
Step 7: BlogPage.js
We will now code the BlogPage.js file. This file will contain the main logic of creating, viewing, editing and deleting content. In the industry, this is also known as Create, Read, Update and Delete (CRUD).
Now add this set of import statements to BlogPage.js.import React, { useState } from 'react'; import { View, StyleSheet, Text, TouchableOpacity, SafeAreaView, TextInput, Alert } from "react-native"; import { FontAwesome5 } from '@expo/vector-icons'; import AsyncStorage from '@react-native-async-storage/async-storage';
Notice that we will be using AsyncStorage to get data from the device’s local storage. Now, we will create a functional component called BlogPage. Add the code below after the set of import statements.const BlogPage = ({ route, navigation }) => { return() }
The code above shows the most basic structure of a component in React and React Native. Do take note that in any programming language, when writing a function, any code that comes after the return statement will not be processed. As such, in JavaScript, we will write any states, logics or functions before the return() statement.
In between the function declaration and the return statement, or after line 6, add the code below.const { blog } = route.params || {}; const [title, setTitle] = useState(blog ? blog.title : ''); const [content, setContent] = useState(blog ? blog.content : ''); const [pageState, setPageState] = useState(blog ? 'edit' : 'new'); const [blogData, setBlogData] = useState(blog ? blog : {});
To check if you have added the code correctly, you may check 'working code' at the end of this step.
The code above sets up various states that will make up how we store the data temporarily before saving it locally. Think of states as variables. In the first line of the code snippet above, we are destructuring blog. Destructuring is when you are trying to assign a value of a variable which happens to be the key of an object. Below is the code sample of what destructuring is.let wallet = { color: “white”, value: 10.50 } let { color } = wallet // the value of “color” will be white, since it is also a property of wallet
Whenever we pass data from one object to another, we can always access the data transferred via the route.params. We are checking if the previous page, IndexPage.js, has passed any selected blog to this page. If so, blog will be populated with the data passed from the previous screen, else, blog will just be an empty object {}.
In the next 4 states you see after it, we will create those states and use a condition statement to initialise the initial value of our state. Any value within the round brackets of useState will be stored as the state's initial value when the page first loads.
For example, when creating a counter application, a counter state will be created using the code below. Notice that the “0” is placed within useState. This is because we want to first set the value of the counter, our state, to start from 0. counter is a read-only variable. It is only allowed to be used in logical calculation and it can be displayed on screen, but to change the value, we will need to use the setCounter function to change the value of state.const [counter, setCounter] = useState(0)
With this in mind, for our line 9, content is the read-only variable state and setContent() is the corresponding function that will be able to change the value of content. However, you may also notice this syntax being used in the useState() method. In the later part of line 9, blog? blog.content : '' checks if the previous screen passed on any blog content to the BlogPage.js file to display. This statement is the shorthand version of an if-else statement.
Ultimately, we are creating a code that checks if the blog object is empty. If it is empty, we will set the initial value of content to an empty string. However, if the IndexPage.js page has passed on blog data, we will set the content to the content property of blog. This is likewise for the other states.
Working code
Step 8: CUD Operations
We will now move on to creating 3 crucial functions. They are editBlog, deleteBlog and createBlog. Now, add the code shown below after line 11. const newBlog = async () => { if (!title || !content) { Alert.alert("Error", "Please fill all the fields", [ { text: "OK" } ]) return; } let blogs = await AsyncStorage.getItem('blogs'); blogs = JSON.parse(blogs) || []; let blogTempData = { title: title, content: content, id: new Date().getTime().toString() } blogs.push(blogTempData); await AsyncStorage.setItem('blogs', JSON.stringify(blogs)); Alert.alert("Blog Saved", "Your blog has been saved successfully", [ { text: "OK", onPress: () => navigation.navigate('Index') } ]) } const editBlog = async () => { let blogs = await AsyncStorage.getItem('blogs'); blogs = JSON.parse(blogs); let blogTempData = { ...blogData, title: title, content: content, } let blogIndex = blogs.findIndex((blog) =>
blog.id
===
blogData.id
); blogs[blogIndex] = blogTempData; await AsyncStorage.setItem('blogs', JSON.stringify(blogs)); Alert.alert("Blog Saved", "Your blog has been saved successfully", [ { text: "OK", onPress: () => navigation.navigate('Index') } ]) } const deleteBlog = async () => { let blogs = await AsyncStorage.getItem('blogs'); blogs = JSON.parse(blogs); let blogIndex = blogs.findIndex((blog) =>
blog.id
===
blogData.id
); blogs.splice(blogIndex, 1); await AsyncStorage.setItem('blogs', JSON.stringify(blogs)); Alert.alert("Blog Deleted", "Your blog has been deleted successfully", [ { text: "OK", onPress: () => navigation.navigate('Index') } ]) }
Creating a Blog
To create a blog, we will create an object for each blog that the user has written. We will store all of the blogs in an array and save the array to our local storage (line 33). We will first format our blog to store the relevant content such as the title and the content. Apart from that, we will also need to create and assign a unique ID to our blog (line 30). For this, we will get the local time and convert the time to epoch time which will be a large number. Epoch is the number of seconds that have passed since 1st January 1970, midnight.
Assuming that the user will only be able to create a blog with at least 1 second between each blog creation, we can safely assume that the ID is unique. After we have set up our blog schema, we will fetch the existing blogs from the local storage. In line 25, notice the || character, which means a check will be performed for any blogs stored locally for the very first time. If there are no blogs stored, we will initialise an empty array. Using the push() method, we will be able to add to the blogs and store them locally using AsyncStorage.setItem() in line 53.
Editing a Blog
The code provided here shows how to edit a blog in the same fashion as the creation of a blog. The difference is instead of pushing new data into the blogs array, we will update the data by first finding the blog index using the findIndex" method. In this method, we will compare the id of the blog in the array to the id of the blog that we want to edit. If they are the same, we will update the corresponding blog data. After updating the blog data, we will store the blogs in the local storage by converting them into a string using JSON.stringify.
Deleting a Blog
Similarly to the editing process, the code for deleting a blog first fetches the existing blogs from the local storage. After that, it finds the index of the blog that we want to delete. The splice method is used to remove an element from an array. After removing the desired blog, we will store the updated blogs in the local storage by converting them into a string using JSON.stringify.
Step 9: BlogPage.js Structure and Styling
In line 75, replace return() with the code below. return ( <View> <View style={{ backgroundColor: "#191B31", padding: 10, marginTop: StatusBar.currentHeight }}> <SafeAreaView> <View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}> <TouchableOpacity onPress={()=>{ if (pageState === "new") { newBlog() }else{ editBlog() } }} > <FontAwesome5 name="check" size={24} color="white" /> </TouchableOpacity> <Text style={{ color: '#fff', fontSize: 30, fontWeight: 'bold', textAlign: "center" }}>{pageState === "new" ? (title ? title : "New Blog") : "Edit Blog"}</Text> {pageState === "edit" ? <TouchableOpacity onPress={() => { deleteBlog() }} > <FontAwesome5 name="trash" size={24} color="white" /> </TouchableOpacity> : <View></View>} </View> </SafeAreaView> </View> <View style={styles.container}> <SafeAreaView> <TextInput style={styles.input} placeholder="write something interesting..." placeholderTextColor={'#777'} onChangeText={text => setTitle(text)} value={title} /> <TextInput style={{ ...styles.input, fontSize: 16, marginTop: 20 }} placeholder="explain your thoughts..." placeholderTextColor={'#777'} onChangeText={text => setContent(text)} value={content} multiline={true} /> </SafeAreaView> </View> </View> );
In the code above, you may notice that we will only show the “Delete” button (line 94) when the blog has been created. We will be able to determine it by using the pageState state. From there, we will show and hide the button based on the conditional statement.
In line 2, add "StatusBar'' after "Alert," as we need to import it to ensure that our blog page has a top margin of the height of the status bar (line 77). Your line 2 should now look like this.import { View, StyleSheet, Text, TouchableOpacity, SafeAreaView, TextInput, Alert, StatusBar } from "react-native";
Last but not the least, let's add the styling after the BlogPage functional component at line 127.const styles = StyleSheet.create({ container: { height: '100%', backgroundColor: '#031031', padding: 10, }, input: { color: '#fff', fontSize: 20, }, }); export default BlogPage;
To check if you have added the code correctly, you may check your code against 'working code' below.
Working code
Step 10: Final Touches
Lastly, in the IndexPage.js file, we will need to fetch the data stored in the local storage and display them on screen. We will display each blog entry as a row and whenever the user clicks the row, we will pass the row data to BlogPage.js.
Open IndexPage.js and add these import statements after the other import statements at the top of the file.import React, { useState, useEffect } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage';
Then, add this before the return statement (line 7) in the IndexPage function.const [blogData, setBlogData] = useState([]) const getBlogs = async () => { let blogs = await AsyncStorage.getItem('blogs'); blogs = JSON.parse(blogs) || []; setBlogData(blogs); } useEffect(() => { const subscriber = navigation.addListener('focus', () => { getBlogs(); }); return subscriber; }, [])
We will create a state to store all of the data fetched from the local storage. In line 7, we will initialise the initial value of blogData to an empty array. Next, in line 9, we will create a function that will get the blogs from our AsyncStorage.
useEffect is a hook that comes built in with React and React Native. It is a hook that will run the code given each time the page has been loaded. For this case, we will call the function, getBlogs each time the page is opened. Once getBlogs has fetched the blogs, it will use the setBlogData() function to store the data into the state.
Add this code below the <TouchableOpacity> component, after line 37.
<ScrollView> {/* Blog List */} {blogData&&blogData.map((blog) => { return ( <TouchableOpacity key={blog.id} style={{ marginTop:10, backgroundColor:"white", borderRadius: 10, padding:10}} onPress={() => navigation.navigate('BlogPage', { blog: blog })} > <Text style={{ fontSize: 15, fontWeight:"bold"}}>{blog.title}</Text> <Text style={{ }}>{blog.content}</Text> </TouchableOpacity> ) })} </ScrollView>
This code displays a list of blogs by mapping over the blogData array and displaying each blog in a <TouchableOpacity> component. Each component has a unique key that is assigned to it by the blog's ID. The title and the content of the blog are displayed as Text components within the <TouchableOpacity> component. The <TouchableOpacity> component is designed to be interactive. It listens for a press event and when triggered, navigates the user to the BlogPage screen and passes the blog data as a parameter. This allows the user to view and edit the blog details in the BlogPage screen. The component is placed inside a <ScrollView> component, which enables the user to scroll down to view all of the blogs.
With the addition of the <ScrollView> component, we will also need to import it from React Native. In line 1, after TouchableOpacity and before '}', add ScrollView.
To check that you have correctly added the three code chunks in this step, you may check your code against 'working code' below.