How to build the most beautiful Todolist with React Native and Socket.io 🎉
Todolist is a simple task list where you mark everything you need to do and the status of it as "Finished / Not Finished".
What is this article about?
In this article, you’ll learn how to build a to-do list application that allows you to sign in, create and delete a to-do, and add comments to each to-do using React Native and Socket.io.
Why Socket.io?
If you are reading this, you have probably wondered – I can do it with a Restful API. So why do I need to use Socket.io?
We want to make a todo list where the user can create a todo list for other users and let them see the status online without refreshing the page.
Socket.io is a highly performant JavaScript library that allows us to create real-time, bi-directional communication between web browsers and a Node.js server. It follows the WebSocket protocol and provides better functionalities, such as fallback to HTTP long-polling or automatic reconnection, which enables us to build efficient real-time applications.
Novu – the first open-source notification infrastructure
Just a quick background about us. Novu is the first open-source notification infrastructure. We basically help to manage all the product notifications. It can be In-App (the bell icon like you have in Facebook – Websockets), Emails, SMSs and so on.
I would be super happy if you could give us a star! And let me also know in the comments ❤️
https://github.com/novuhq/novu
How to connect React Native to a Socket.io server
Here, you’ll learn how to connect the to-do list application to a Socket.io server. In this guide, I’ll be using Expo – a tool that provides an easier way of building React Native applications.
Creating a React Native app with Expo
Expo saves us from the complex configurations required to create a native application with the React Native CLI, making it the easiest and fastest way to build and publish React Native apps.
Ensure you have the Expo CLI, Node.js, and Git installed on your computer. Then, create the project folder and an Expo React Native app by running the code below.
1mkdir todolist-app
2cd todolist-app
3expo init app
Expo allows us to create native applications using the Managed or Bare Workflow. We’ll use the blank Managed Workflow in this tutorial.
1? Choose a template: › - Use arrow-keys. Return to submit.
2 ----- Managed workflow -----
3❯ blank a minimal app as clean as an empty canvas
4 blank (TypeScript) same as blank but with TypeScript configuration
5 tabs (TypeScript) several example screens and tabs using react-navigation and TypeScript
6 ----- Bare workflow -----
7 minimal bare and minimal, just the essentials to get you started
Install Socket.io Client API to the React Native app.
1cd app
2expo install socket.io-client
Create a socket.js
file within a utils folder.
1mkdir utils
2touch socket.js
Then, copy the code below into the socket.js
file.
1import { io } from "socket.io-client";
2
3const socket = io.connect("http://localhost:4000");
4export default socket;
The code snippet above creates a real-time connection to the server hosted at that URL. (We’ll set up the server in the upcoming section).
Create a styles.js
file within the utils folder and copy the code below into the file. It contains all the styling for the application.
1import { StyleSheet } from "react-native";
2
3export const styles = StyleSheet.create({
4 screen: {
5 flex: 1,
6 backgroundColor: "#fff",
7 padding: 10,
8 },
9 header: {
10 padding: 10,
11 justifyContent: "space-between",
12 flexDirection: "row",
13 marginBottom: 20,
14 },
15 heading: {
16 fontSize: 24,
17 fontWeight: "bold",
18 },
19 container: {
20 padding: 15,
21 },
22 loginScreen: {
23 flex: 1,
24 },
25 loginContainer: {
26 flex: 1,
27 padding: 10,
28 flexDirection: "column",
29 justifyContent: "center",
30 },
31 textInput: {
32 borderWidth: 1,
33 width: "100%",
34 padding: 12,
35 marginBottom: 10,
36 },
37 loginButton: {
38 width: 150,
39 backgroundColor: "#0D4C92",
40 padding: 15,
41 },
42 todoContainer: {
43 flexDirection: "row",
44 justifyContent: "space-between",
45 backgroundColor: "#CDF0EA",
46 padding: 15,
47 borderRadius: 10,
48 marginBottom: 10,
49 },
50 todoTitle: {
51 fontWeight: "bold",
52 fontSize: 18,
53 marginBottom: 8,
54 },
55 subTitle: {
56 opacity: 0.6,
57 },
58 form: {
59 flexDirection: "row",
60 marginBottom: 40,
61 },
62 input: {
63 borderWidth: 1,
64 padding: 12,
65 flex: 1,
66 justifyContent: "center",
67 },
68 modalScreen: {
69 backgroundColor: "#fff",
70 flex: 1,
71 padding: 10,
72 alignItems: "center",
73 },
74 textInput: {
75 borderWidth: 1,
76 padding: 10,
77 width: "95%",
78 marginBottom: 15,
79 },
80 modalButton: {
81 backgroundColor: "#0D4C92",
82 padding: 10,
83 },
84 buttonText: {
85 fontSize: 18,
86 textAlign: "center",
87 color: "#fff",
88 },
89 comment: { marginBottom: 20 },
90 message: {
91 padding: 15,
92 backgroundColor: "#CDF0EA",
93 width: "80%",
94 borderRadius: 10,
95 },
96});
Install React Navigation and its dependencies. React Navigation allows us to move from one screen to another within a React Native application.
1npm install @react-navigation/native
2npx expo install react-native-screens react-native-safe-area-context
Setting up the Node.js server
Here, I will guide you through creating the Socket.io Node.js server for real-time communication.
Create a server
folder within the project folder.
1cd todolist-app
2mkdir server
Navigate into the server
folder and create a package.json
file.
1cd server & npm init -y
Install Express.js, CORS, Nodemon, and Socket.io Server API.
1npm install express cors nodemon socket.io
Express is a fast, minimalist framework that provides several features for building web applications in Node.js. CORS is a Node.js package that allows communication between different domains.
Nodemon is a Node.js tool that automatically restarts the server after detecting file changes, and Socket.io allows us to configure a real-time connection on the server.
Create an index.js
file – the entry point to the Node.js server.
1touch index.js
Set up a simple Node.js server using Express.js. The code snippet below returns a JSON object when you visit the http://localhost:4000/api
in your browser.
1//👇🏻 index.js
2const express = require("express");
3const app = express();
4const PORT = 4000;
5
6app.use(express.urlencoded({ extended: true }));
7app.use(express.json());
8
9app.get("/api", (req, res) => {
10 res.json({
11 message: "Hello world",
12 });
13});
14
15app.listen(PORT, () => {
16 console.log(`Server listening on ${PORT}`);
17});
Next, add Socket.io to the project to create a real-time connection. Before the app.get()
block, copy the code below.
1//👇🏻 New imports
2.....
3const socketIO = require('socket.io')(http, {
4 cors: {
5 origin: "http://localhost:3000"
6 }
7});
8
9//👇🏻 Add this before the app.get() block
10socketIO.on('connection', (socket) => {
11 console.log(`⚡: ${socket.id} user just connected!`);
12
13 socket.on('disconnect', () => {
14 socket.disconnect()
15 console.log('🔥: A user disconnected');
16 });
17});
From the code snippet above, the socket.io("connection")
function establishes a connection with the React app, creates a unique ID for each socket, and logs the ID to the console whenever you refresh the app.
When you refresh or close the app, the socket fires the disconnect event showing that a user has disconnected from the socket.
Configure Nodemon by adding the start command to the list of scripts in the package.json
file. The code snippet below starts the server using Nodemon.
1//👇🏻 In server/package.json
2
3"scripts": {
4 "test": "echo \\"Error: no test specified\\" && exit 1",
5 "start": "nodemon index.js"
6 },
You can now run the server with Nodemon by using the command below.
1npm start
Building the app user interface
In this section, we’ll create the user interface for the to-do list application to enable users to sign in to the application, create and delete to-dos, and add comments to each to-do.
First, let’s set up React Navigation.
Create a screens folder within the app folder, and add the Home, Login, and Comments components. Render a “Hello World” text within them.
1mkdir screens
2cd screens
3touch Home.js Login.js Comments.js
Copy the code below into the App.js
file within the app folder.
1//👇🏻 the app components
2import Home from "./screens/Home";
3import Comments from "./screens/Comments";
4import Login from "./screens/Login";
5
6//👇🏻 React Navigation configurations
7import { NavigationContainer } from "@react-navigation/native";
8import { createNativeStackNavigator } from "@react-navigation/native-stack";
9
10const Stack = createNativeStackNavigator();
11
12export default function App() {
13 return (
14 <NavigationContainer>
15 <Stack.Navigator>
16 <Stack.Screen
17 name='Login'
18 component={Login}
19 options={{ headerShown: false }}
20 />
21 <Stack.Screen
22 name='Home'
23 component={Home}
24 options={{ headerShown: false }}
25 />
26 <Stack.Screen name='Comments' component={Comments} />
27 </Stack.Navigator>
28 </NavigationContainer>
29 );
30}
The Login screen
Copy the code below into the Login.js
file.
1import {
2 View,
3 Text,
4 SafeAreaView,
5 StyleSheet,
6 TextInput,
7 Pressable,
8} from "react-native";
9
10import React, { useState } from "react";
11
12const Login = ({ navigation }) => {
13 const [username, setUsername] = useState("");
14
15 const handleLogin = () => {
16 if (username.trim()) {
17 console.log({ username });
18 } else {
19 Alert.alert("Username is required.");
20 }
21 };
22
23 return (
24 <SafeAreaView style={styles.loginScreen}>
25 <View style={styles.loginContainer}>
26 <Text
27 style={{
28 fontSize: 24,
29 fontWeight: "bold",
30 marginBottom: 15,
31 textAlign: "center",
32 }}
33 >
34 Login
35 </Text>
36 <View style={{ width: "100%" }}>
37 <TextInput
38 style={styles.textInput}
39 value={username}
40 onChangeText={(value) => setUsername(value)}
41 />
42 </View>
43 <Pressable onPress={handleLogin} style={styles.loginButton}>
44 <View>
45 <Text style={{ color: "#fff", textAlign: "center", fontSize: 16 }}>
46 SIGN IN
47 </Text>
48 </View>
49 </Pressable>
50 </View>
51 </SafeAreaView>
52 );
53};
54
55export default Login;
The code snippet accepts the username from the user and logs it on the console.
Next, update the code and save the username using Async Storage for easy identification.
Async Storage is a React Native package used to store string data in native applications. It is similar to the local storage on the web and can be used to store tokens and data in string format.
Run the code below to install Async Storage.
1expo install @react-native-async-storage/async-storage
Update the handleLogin
function to save the username via AsyncStorage.
1import AsyncStorage from "@react-native-async-storage/async-storage";
2
3const storeUsername = async () => {
4 try {
5 await AsyncStorage.setItem("username", username);
6 navigation.navigate("Home");
7 } catch (e) {
8 Alert.alert("Error! While saving username");
9 }
10};
11
12const handleLogin = () => {
13 if (username.trim()) {
14 //👇🏻 calls AsyncStorage function
15 storeUsername();
16 } else {
17 Alert.alert("Username is required.");
18 }
19};
The Home screen
Update the Home.js
file to contain the code snippet below:
1import { SafeAreaView, Text, StyleSheet, View, FlatList } from "react-native";
2import { Ionicons } from "@expo/vector-icons";
3import React, { useState } from "react";
4import Todo from "./Todo";
5import ShowModal from "./ShowModal";
6
7const Home = () => {
8 const [visible, setVisible] = useState(false);
9
10//👇🏻 demo to-do lists
11 const [data, setData] = useState([
12 { _id: "1", title: "Hello World", comments: [] },
13 { _id: "2", title: "Hello 2", comments: [] },
14 ]);
15
16 return (
17 <SafeAreaView style={styles.screen}>
18 <View style={styles.header}>
19 <Text style={styles.heading}>Todos</Text>
20 <Ionicons
21 name='create-outline'
22 size={30}
23 color='black'
24 onPress={() => setVisible(!visible)}
25 />
26 </View>
27 <View style={styles.container}>
28 <FlatList
29 data={data}
30 keyExtractor={(item) => item._id}
31 renderItem={({ item }) => <Todo item={item} />}
32 />
33 </View>
34 <ShowModal setVisible={setVisible} visible={visible} />
35 </SafeAreaView>
36 );
37};
38
39export default Home;
From the code snippet above, we imported two components, Todo
, and ShowModal
as sub-components within the Home component. Next, let’s create the Todo
and ShowModal
components.
1touch Todo.js ShowModal.js
Update the Todo.js
file to contain the code below. It describes the layout for each to-do.
1import { View, Text, StyleSheet } from "react-native";
2import { React } from "react";
3import { AntDesign } from "@expo/vector-icons";
4
5const Todo = ({ item }) => {
6 return (
7 <View style={styles.todoContainer}>
8 <View>
9 <Text style={styles.todoTitle}>{item.title}</Text>
10 <Text style={styles.subTitle}>View comments</Text>
11 </View>
12 <View>
13 <AntDesign name='delete' size={24} color='red' />
14 </View>
15 </View>
16 );
17};
18
19export default Todo;
Update the ShowModal.js
file to contain the code below:
1import {
2 Modal,
3 View,
4 Text,
5 StyleSheet,
6 SafeAreaView,
7 TextInput,
8 Pressable,
9} from "react-native";
10import React, { useState } from "react";
11
12const ShowModal = ({ setVisible, visible }) => {
13 const [input, setInput] = useState("");
14
15 const handleSubmit = () => {
16 if (input.trim()) {
17 console.log({ input });
18 setVisible(!visible);
19 }
20 };
21
22 return (
23 <Modal
24 animationType='slide'
25 transparent={true}
26 visible={visible}
27 onRequestClose={() => {
28 Alert.alert("Modal has been closed.");
29 setVisible(!visible);
30 }}
31 >
32 <SafeAreaView style={styles.modalScreen}>
33 <TextInput
34 style={styles.textInput}
35 value={input}
36 onChangeText={(value) => setInput(value)}
37 />
38
39 <Pressable onPress={handleSubmit} style={styles.modalButton}>
40 <View>
41 <Text style={styles.buttonText}>Add Todo</Text>
42 </View>
43 </Pressable>
44 </SafeAreaView>
45 </Modal>
46 );
47};
48
49export default ShowModal;
The code snippet above represents the modal that pops up when you press the icon for creating a new to-do.
The Comments screen
Copy the code snippet below into the Comments.js
file.
1import React, { useLayoutEffect, useState } from "react";
2import { View, StyleSheet, TextInput, Button, FlatList } from "react-native";
3import AsyncStorage from "@react-native-async-storage/async-storage";
4import CommentUI from "./CommentUI";
5
6const Comments = ({ navigation, route }) => {
7 const [comment, setComment] = useState("");
8 const [commentsList, setCommentsList] = useState([
9 {
10 id: "1",
11 title: "Thank you",
12 user: "David",
13 },
14 {
15 id: "2",
16 title: "All right",
17 user: "David",
18 },
19 ]);
20 const [user, setUser] = useState("");
21
22 // fetches the username from AsyncStorage
23 const getUsername = async () => {
24 try {
25 const username = await AsyncStorage.getItem("username");
26 if (username !== null) {
27 setUser(username);
28 }
29 } catch (err) {
30 console.error(err);
31 }
32 };
33
34 // runs on page load
35 useLayoutEffect(() => {
36 getUsername();
37 }, []);
38
39 // logs the comment details to the console
40 const addComment = () => console.log({ comment, user });
41
42 return (
43 <View style={styles.screen}>
44 <View style={styles.form}>
45 <TextInput
46 style={styles.input}
47 value={comment}
48 onChangeText={(value) => setComment(value)}
49 multiline={true}
50 />
51 <Button title='Post Comment' onPress={addComment} />
52 </View>
53
54 <View>
55 <FlatList
56 data={commentsList}
57 keyExtractor={(item) => item.id}
58 renderItem={({ item }) => <CommentUI item={item} />}
59 />
60 </View>
61 </View>
62 );
63};
64
65export default Comments;
The code snippet above contains a sub-component, CommentUI
– which represents the layout for each comment.
Update the CommentUI
component as below:
1import { View, Text, StyleSheet } from "react-native";
2import React from "react";
3
4const CommentUI = ({ item }) => {
5 return (
6 <View style={styles.comment}>
7 <View style={styles.message}>
8 <Text style={{ fontSize: 16 }}>{item.title}</Text>
9 </View>
10
11 <View>
12 <Text>{item.user}</Text>
13 </View>
14 </View>
15 );
16};
17
18export default CommentUI;
Sending real-time data via Socket.io
In this section, you’ll learn how to send data between the React Native application and a Socket.io server.
How to create a new to-do
Import socket from the socket.js
file into the ShowModal.js
file.
1import socket from "../utils/socket";
Update the handleSubmit
function to send the new to-do to the server.
1//👇🏻 Within ShowModal.js
2const handleSubmit = () => {
3 if (input.trim()) {
4 //👇🏻 sends the input to the server
5 socket.emit("addTodo", input);
6 setVisible(!visible);
7 }
8};
Create a listener to the addTodo
event on the server that adds the to-do to an array on the backend.
1//👇🏻 array of todos
2const todoList = [];
3
4//👇🏻 function that generates a random string as ID
5const generateID = () => Math.random().toString(36).substring(2, 10);
6
7socketIO.on("connection", (socket) => {
8 console.log(`⚡: ${socket.id} user just connected!`);
9
10 // 👇🏻 listener to the addTodo event
11 socket.on("addTodo", (todo) => {
12 //👇🏻 adds the todo to a list of todos
13 todoList.unshift({ _id: generateID(), title: todo, comments: [] });
14 //👇🏻 sends a new event containing the todos
15 socket.emit("todos", todoList);
16 });
17
18 socket.on("disconnect", () => {
19 socket.disconnect();
20 console.log("🔥: A user disconnected");
21 });
22});
How to display the to-dos
Import socket from the socket.js
file into the Home.js
file.
1import socket from "../utils/socket";
Create an event listener to the to-dos created on the server and render them on the client.
1const [data, setData] = useState([]);
2
3useLayoutEffect(() => {
4 socket.on("todos", (data) => setData(data));
5}, [socket]);
The todos
event is triggered only when you create a new to-do. Next, create a route on the server that returns the array of to-dos so you can fetch them via API request within the app.
Update the index.js
file on the server to send the to-do list via an API route as below.
1app.get("/todos", (req, res) => {
2 res.json(todoList);
3});
Add the code snippet below to the Home.js
file:
1//👇🏻 fetch the to-do list on page load
2useLayoutEffect(() => {
3 function fetchTodos() {
4 fetch("http://localhost:4000/todos")
5 .then((res) => res.json())
6 .then((data) => setData(data))
7 .catch((err) => console.error(err));
8 }
9 fetchTodos();
10}, []);
How to delete the to-dos
From the image below, there is a delete icon beside each to-do. When you press the button, the selected todo should be deleted on both the server and within the app.
Navigate to the Todo.js
file and import Socket.io.
1import socket from "../utils/socket";
Create a function – deleteTodo
that accepts the to-do id when you press the delete icon and sends it to the server.
1import { View, Text, StyleSheet } from "react-native";
2import { React } from "react";
3import { AntDesign } from "@expo/vector-icons";
4import { useNavigation } from "@react-navigation/native";
5import socket from "../utils/socket";
6
7const Todo = ({ item }) => {
8 const navigation = useNavigation();
9
10 //👇🏻 deleteTodo function
11 const deleteTodo = (id) => socket.emit("deleteTodo", id);
12
13 return (
14 <View style={styles.todoContainer}>
15 <View>
16 <Text style={styles.todoTitle}>{item.title}</Text>
17 <Text
18 style={styles.subTitle}
19 onPress={() =>
20 navigation.navigate("Comments", {
21 title: item.title,
22 id: item._id,
23 })
24 }
25 >
26 View comments
27 </Text>
28 </View>
29 <View>
30 <AntDesign
31 name='delete'
32 size={24}
33 color='red'
34 onPress={() => deleteTodo(item._id)}
35 />
36 </View>
37 </View>
38 );
39};
40
41export default Todo;
Delete the to-do via its ID.
1socket.on("deleteTodo", (id) => {
2 let result = todoList.filter((todo) => todo._id !== id);
3 todoList = result;
4 //👇🏻 sends the new todo list to the app
5 socket.emit("todos", todoList);
6});
Adding and displaying comments
When you click the View comments
text, it navigates to the Comments page – where you can view all the comments related to the to-do.
1<Text
2 style={styles.subTitle}
3 onPress={() =>
4 navigation.navigate("Comments", {
5 title: item.title,
6 id: item._id,
7 })
8 }
9>
10 View comments
11</Text>
The navigation function accepts the title and id of the selected to-do as parameters; because we want the to-do title at the top of the route and also fetch its comments from the server via its ID.
To achieve this, update the useLayoutEffect
hook within the Comments.js
file to change the route’s title and send the ID to the server.
1useLayoutEffect(() => {
2 //👇🏻 update the screen's title
3 navigation.setOptions({
4 title: route.params.title,
5 });
6 //👇🏻 sends the todo's id to the server
7 socket.emit("retrieveComments", route.params.id);
8
9 getUsername();
10}, []);
Listen to the retrieveComments
event and return the to-do’s comments.
1socket.on("retrieveComments", (id) => {
2 let result = todoList.filter((todo) => todo._id === id);
3 socket.emit("displayComments", result[0].comments);
4});
Add another useLayoutEffect
hook within the Comments.js
file that updates the comments when it is retrieved from the server.
1useLayoutEffect(() => {
2 socket.on("displayComments", (data) => setCommentsList(data));
3}, [socket]);
To create new comments, update the addComment
function by sending the comment details to the server.
1const addComment = () =>{
2 socket.emit("addComment", { comment, todo_id: route.params.id, user});
3}
Create the event listener on the server and add the comment to the list of comments.
1socket.on("addComment", (data) => {
2 //👇🏻 Filters the todo list
3 let result = todoList.filter((todo) => todo._id === data.todo_id);
4 //👇🏻 Adds the comment to the list of comments
5 result[0].comments.unshift({
6 id: generateID(),
7 title: data.comment,
8 user: data.user,
9 });
10 //👇🏻 Triggers this event to update the comments on the UI
11 socket.emit("displayComments", result[0].comments);
12});
Congratulations!🥂 You’ve completed the project for this tutorial.
Conclusion
So far, you’ve learnt how to set up Socket.io in a React Native and Node.js application, save data with Async Storage, and communicate between a server and the Expo app via Socket.io.
This project is a demo of what you can build using React Native and Socket.io. Feel free to improve the project by using an authentication library and a database that supports real-time storage.
The source code for this application is available here: https://github.com/novuhq/blog/tree/main/build-todolist-with-reactnative
Thank you for reading!
Help me out!
If you feel like this article helped you understand WebSockets better! I would be super happy if you could give us a star! And let me also know in the comments ❤️
https://github.com/novuhq/novu