Hello, fellow dev đź‘‹ Today we are going to create a to-do list with React. I use Visual Studio Code and Node.js. So make sure you have those installed, and let's start!
If you’d prefer to skip the details and get a quick overview, check out the video below!
Step 1:
Create a new React App:
Let's create our React app by running the following command:
npx create-react-app mytodo
After long waiting, we have all our necessary React dependencies installed. So we are ready to code. I am going to clean up and get rid of all unnecessary files from the src folder. After that our project should look like this:
todo
-node_modules
-public
-src
-App.js
-App.css
-index.js
-package.json
Step 2:
Create our Components
Since React is component-based, I am going to utilize this feature and create a few components, such as Header, List, and Task. The Header component will be responsible for displaying the time and date, List will handle adding, editing, and deleting logic, and Task will handle marking our task complete.
Also, I want to add a util.js for additional formatting for my header and dates.
Here is the updated file structure for our app:
-node_modules
-public
-src
-components
-Header.js
-List.js
-Task.js
-App.js
-App.css
-index.js
-util.js
-package.json
Step 3: Update Header.js and set up Util.js
First, we need to create a file that will help us to perform date and time formatting, plus additional functionality for our project - util.js
Inside util.js
//util.js
//Handle date conversion
const DateHandling = (month, day) => {
//Create array that holds 12 months
const months = ["January", "February", "March",
"April", "May", "June", "July", "August",
"September","October", "November", "December"]
//Create array that holds days of the week
const weekDay = ["Monday", "Tuesday",
"Wednesday","Thursday","Friday",
"Saturday", "Sunday"]
return [months[month], weekDay[day]]
}
//We want to make sure that our task description
//will be less than 120 characters, but more than 0
const ValidateInput = (message) => {
if(message.length < 120 & message.length > 0){
return true
}
else{
return false
}
}
export {DateHandling, ValidateInput};
Inside our Header.js we are going to add the following code:
// Header.js
import { useEffect, useState } from 'react'
import { DateHandling } from '../util'
import './styles/Header.css'
const Header = () => {
/* Creating a new date and using our
utilities method to extract our date info */
let todayDate = new Date()
let convertedDate = DateHandling(todayDate.getMonth(), todayDate.getDay())
/* We need to store our date state and update when needed,
so we use React Hook 'useState'.
We set our day to today, extract the month, and assign time */
const [today, setToday] = useState({
day: todayDate.getDate(),
month: convertedDate[0],
time: todayDate.toLocaleTimeString()
})
/* Here we handle our formatting and make sure we display
our time correctly */
const formatTime = (time) => {
let timeFirstHalf = time.split(":")
let timePeriod = timeFirstHalf[2].split(" ")
let formattedTime = `${timeFirstHalf[0]}: ${timeFirstHalf[1]} ${timePeriod[1]}`
return formattedTime
}
// We create a separate function to update time
const updateTime = () => {
setToday((prev) => ({
...prev,
time: formatTime(new Date().toLocaleTimeString())
}))
}
// useEffect hook helps us update our time every 60 seconds
useEffect(() => {
updateTime()
const intervalId = setInterval(updateTime, 60000)
return () => clearInterval(intervalId)
}, [])
// Here is our actual JSX, we display month, day, and time.
return (
<header>
<h1>Today</h1>
<div>
<h2>{today.month} {today.day}</h2>
<h2>{today.time}</h2>
</div>
</header>
)
}
export default Header
Step 4: Add Task Component
We have to separate our single task from the rest of the application, and we created Task.js for that. This component will accept a few properties, such as task description, whether it is in update mode, and whether it is still relevant.
I am going to use FontAwesome Icons; the easiest way to integrate it is to install it with npm:
npm install @fortawesome/react-fontawesome
npm install @fortawesome/free-solid-svg-icons
After that, we are ready to create our task component.
// Task.js
// Import React Hooks, and additional Font Awesome icons
import { useState } from 'react'
// Import the library for our Font Awesome
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
// Name of the icon
import { faCheck } from '@fortawesome/free-solid-svg-icons'
// Define our Task with 3 properties
const Task = ({ taskDescription, isActive, inUpdateMode }) => {
// We want to hold state for our task, if it is completed we want
// to reflect it
const [isCompleted, setTaskState] = useState(false)
const markTask = () => {
if (!isCompleted) {
setTaskState(true)
} else {
setTaskState(false)
}
}
// When user clicks on the task checkmark
// it will mark/unmark the task as completed
return (
<div id='single-task-container'>
<div id="task_description_container">
<div id="task_point" onClick={markTask}>
{isCompleted ?
<FontAwesomeIcon icon={faCheck} id="checkmark"/> : ' '}
</div>
{/* If we click on task itself we will move to updateMode
Where we can edit description and save the new value */}
<p onClick={inUpdateMode}>{taskDescription}</p>
</div>
<span onClick={isActive}>x</span>
</div>
)
}
export default Task
Step 5: Add List Component
As I mentioned above, we need to handle logic for our list, such as adding, editing, or removing items. And the best place for it is our List.js
First of all, we need to import all necessary functions and methods.
//List.js
// We importing our previously created Component
import Task from './Task';
import { useEffect, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowRight } from '@fortawesome/free-solid-svg-icons';
import { ValidateInput } from '../util';
After that we can create our List function. We set our initial state for our current list and assign it to our local storage, so we will be able to access our list even after we refresh the page.
//previous code
// List.js
const List = () => {
// Set state for a new task, for now it is null
const [newTask, setTask] = useState('')
// Our initial state will read data with
// "tasks" from the browser's local storage
const [tasks, setTasks] = useState(() => {
// Getting stored value
const toDos = localStorage.getItem("tasks")
const initialValue = JSON.parse(toDos)
return initialValue || []
})
// We will display an error if any, initial state is null
const [error, setError] = useState(null)
// Handle Task Update
const [inUpdateMode, setUpdateMode] = useState(false)
const [taskforUpdate, setTaskForUpdate] = useState({
index: null,
text: null
})
// Every time we update our task, we will set
// our local storage with new data
useEffect(() => {
localStorage.setItem('tasks', JSON.stringify(tasks))
}, [tasks])
}
Right now we can add, create, edit, and delete functionality for our tasks.
// List.js
const List = () => {
// ...previous code
// We are adding a new task, set error to null,
// and validate the input.
// If everything is good we add the task to the list
// else we display an error
const addNewTask = (e) => {
e.preventDefault()
setError(null)
if (ValidateInput(newTask)) {
setTasks(prev => [...prev, newTask])
setTask('')
} else {
setError("Make sure your input is correct!")
}
}
// We use task index and create a new list
// with tasks without the selected one
const removeTask = (index) => {
let newList = tasks.filter((_, i) => i !== index)
setTasks(newList)
}
const handleUpdate = (index, text) => {
setUpdateMode(true)
setTaskForUpdate((prevState) => ({
...prevState,
index: index,
text: text
}))
}
// We use index and new text to change the description
// If new description passes validation, we reassign the description
const updateTask = (index, task) => {
if (ValidateInput(task)) {
let newArray = tasks
newArray[index] = task
setTasks(newArray)
setUpdateMode(false)
} else {
setError("Make sure your input is correct!")
}
}
}
We are done with our main functionality; now let's return the view to the user.
// List.js
const List = () => {
// ...prev code
// create, edit and delete functionality
// Here we define whether we need to display "Edit task" or "Add new task"
return (
<section id="list_section">
<p id="error">{error}</p>
{
inUpdateMode ? (
<div>
<h1>Edit your Task</h1>
<form className="newTask_container"
onSubmit={(e) => {
e.preventDefault()
updateTask(taskforUpdate.index, taskforUpdate.text)
}}>
<input
type='text'
value={taskforUpdate.text}
onChange={(e) => {
setTaskForUpdate((prevState) =>
({ ...prevState, text: e.target.value }))
setError(null)
}}
/>
<FontAwesomeIcon
icon={faArrowRight}
className="SubmitBtn"
onClick={() => { updateTask(taskforUpdate.index, taskforUpdate.text) }}
/>
</form>
</div>
) : (
<div>
<form className="newTask_container" onSubmit={addNewTask}>
<input
type='text'
placeholder="Add new task..."
value={newTask}
onChange={(e) => {
setTask(e.target.value)
setError(null)
}}
/>
<FontAwesomeIcon
icon={faArrowRight}
className="SubmitBtn"
onClick={addNewTask}
/>
</form>
{tasks.length === 0 ? <h1>No active tasks</h1> :
<h1>Your active tasks</h1>}
{
tasks.map((task, index) =>
<Task
taskDescription={task}
key={index}
inUpdateMode={() => handleUpdate(index, task)}
isActive={() => removeTask(index)}
/>
)
}
</div>
)
}
</section>
)
}
export default List // remember to export your components
Step 6: Update App.js
We did create our component, but you still can’t see many changes on your page; we have to import it to your App.js. Because this is our root component… for now.
Here is the updated app. js file
import './App.css';
import List from './components/List';
import Header from './components/Header';
function App() {
return (
<div className="App">
<div id="app_container">
<Header />
<List />
</div>
</div>
);
}
export default App;
Step 7: Add Styles
For each component, I am going to create its own stylesheet. So inside the component folder, we make a new directory named Styles. Inside the new directory we add 3 stylesheets: Header.css, List.css and Task.css
/*Header.css*/
header{
text-align: left;
width:700px;
margin: auto;
font-family: Roboto;
padding: 10px 30px 0 30px;
color:white;
transition: all 0.5s;
}
header div{
display: flex;
align-items: center;
margin-top: 0.2em;
justify-content: space-between;
}
header h1{
font-weight: 400;
font-size: 1.6em;
}
header h2{
font-weight: 300;
font-size: 1.4em;
text-align: center;
}
Make sure to add this reference to your Header.js.
//Header.js
import './styles/Header.css'
//the rest of the code
Right now you can create and play around with the style for List.css and Task.css. Or you can look at the whole code in the repo.
Thank you for sticking with me during this tutorial. Happy Coding!