Photo by Anete > Lūsiņa on Unsplash
💻 Checkout The Demo - Click Here
📜 Source Code - Click Here
Most of the react development courses for beginners starts with building a todo app 😄 Because it has simple logics yet helps to learn 4 major operations. Some call them CRUD (Create, Read, Update and Delete).
You may have created todo apps earlier with React, but in this tutorial, I will be utilizing React Hooks which was introduced recently to manage state inside functional components.
Generating a React App
There are many ways that you can create a React starter project. I often use yarn
when dealing with dependencies. To get a starter project run
$ yarn create react-app todo-app
then you will be getting a file structure like below 😍
I have deleted logo.svg
App.test.js
serviceWorker.js
and index.css
as I don't use them in these projects. For general styles, I will use App.css
. When you remove them don't forget to remove all imports of them from particular files. If not errors will be shown 😃
Installing Initial Dependencies
I hope to make this little app with antd
which is a UI framework, I love it's simple design and offers a lot of features. Most importantly it's really easy to work with Ant Design. Therefore I'm gonna first install antd
to my project :
$ yarn add antd
before you start working with antd, there is one important thing that you should do, that is importing ants
's CSS file into index.css
file. I will be using Overpass google font with this project, so at the same time, I'm gonna import that also. After importing it looks like below
@import "~antd/dist/antd.css";
@import url("https://fonts.googleapis.com/css?family=Overpass:300,400,700&display=swap");
* {
box-sizing: border-box;
margin: 0;
}
body {
font-family: "Overpass", sans-serif;
}
Okay! For now enough importing 😄 Let's start building our to-do App! First, I'm gonna use the Card
component of antd
and make a square in the middle of our browser screen. Square in the sense a wrapper component to hold our inner components.
import React, { Component } from "react"
import { Card } from "antd"
class App extends Component {
render() {
return <Card title="My Todos">My Todo List</Card>
}
}
export default App
After that, I'm going to apply some changes to the Card element with some custom CSS. There is a way that you can use CSS with React App that is built with create-react-app
that is CSS modules. What I will do is create a file named app.module.css
and import it inside our App.js
. In my app.module.css
file, I will include the following
.app {
max-width: 500px;
margin: 50px auto !important;
}
@media screen and (max-width: 980px) {
.app {
width: 90% !important;
margin: 20px auto !important;
}
}
import React, { Component } from "react"
import { Card } from "antd"
//styles
import styles from "./app.module.css"
class App extends Component {
render() {
return (
<Card className={styles.app} title="My Todos">
My Todo List
</Card>
)
}
}
export default App
By this method, you will be able to write modular css that are very specific to a component and those styles won't affect other modules that have the same class. Suppose if you don't use this method and uses the regular method, which is just writing CSS in a global file, you won't be able to use same class name twice in the app, because you write CSS in a global file that affects the whole project and those CSS rules will apply to all components that have the same CSS name.
After importing you can refer to your styles with dot notation like styles.app
, that's why the className for the Card component is styles.app
and it generates a unique class for that component.
And when you add it and save the file, in the browser it must display like below.
Now we have our base component created inside our App.js file. We mainly need 2 more components,
- A form to add todos
- A todo item with action buttons (to delete, update and make as complete)
Creating TodoForm Element
Before creating out first separated React element, What I'm gonna do is create a folder called components
inside src
folder, I will be storing all my components inside it. That way it's easy to manage components rather than creating all of them inside App.js
file.
I will name my form component as TodoForm
and add the same time I'm adding a separate CSS file for TodoForm
component. After creating all those files, your directory structure will look like below.
📦src
┣ 📂components
┃ ┗ 📂TodoForm
┃ ┃ ┣ 📜TodoForm.js
┃ ┃ ┗ 📜todoForm.module.css
┣ 📜App.js
┣ 📜app.module.css
┣ 📜index.css
┗ 📜index.js
Our TodoForm
component will be a stateful component and we'll be managing state with React Hooks. with React hooks we will be able to bring the state into functional components. It's the latest features that new React version offers us. Let's use it inside our TodoForm
and implement the Component ith the help of antd
Input and form fields.
import React, { useState } from "react"
import { Form, Input, Button } from "antd"
//styles
import styles from "./todoForm.module.css"
const TodoForm = ({ submit }) => {
const [state, setState] = useState({
value: "",
validated: false,
})
const handleSubmit = e => {
e.preventDefault()
if (state.value !== "") {
submit(state.value)
setState({ ...state, value: "", validated: false })
}
}
const handleChange = e => {
setState({
...state,
value: e.target.value,
validated: e.target.value.length > 5 ? true : false,
})
}
return (
<Form layout="inline" onSubmit={handleSubmit} className={styles.form}>
<Form.Item>
<Input
onFocus={() => setState({ ...state, focused: true })}
placeholder="Add Todos ..."
value={state.value}
onChange={handleChange}
className={styles.input}
/>
</Form.Item>
<Button
icon="plus-circle"
type="success"
htmlType="submit"
disabled={!state.validated}
>
Add
</Button>
</Form>
)
}
export default TodoForm
Understanding React Hooks
To use React Hooks, you must import it from react
and call it inside a functional component.
You may have seen that I have called the userState
function right after the function declaration inside the function.
We have to pass 2 parameters as destructuring assignments when you call useState
function and pass any initial state as an Object if you have any. In our case, I have passed value
and validated
attributes.
The state
will always refer to the current state, and we will be using setState
method to manipulate the state. You can give them any names you like, I gave state
and setState
because those are the names that we use inside class components.
For an exmaple you we can do :
const [values, setValues] = useState({
value: "",
validated: false,
})
It's totally up to you to decide what names that you are gonna use, but make sure you use something relative. Don't use random names as it will make other developers confuse 😳 in your compnay 🏢
Handling Events
As this is a small component we only got two events to handle,
onChange
when someone types a todoonSubmit
when someone submits the todo form
Now let's see how we are going to handle those events with the help of our superhero React hooks 🔥
Lets deeply inspect our handleChange
method
const handleChange = e => {
setState({
...state,
value: e.target.value,
validated: e.target.value.length > 5 ? true : false,
})
}
onChange Event
onChange
method is mostly used with input fields. You can see that I have added an onChage
method to the <Input>
component. Whenever someone type on that field handleChange
function will be run and It will receive the event
object which is (e) in our case, that is passed from the Input
field.
And I have implemented the setState()
inside handleChange
function so that it will change our state, every time the input value changes. The changed value is extracted from the e.target.value
, e.terget
will always refer to the Input
element itself and value
property will be equal to the values that we have typed.
Here I do some other thing, which is setting the validated
property, I want the todo to be at least 5 characters long, that's why I check the length
of the input value. Based on that value, I have added a disabled
attribute to the submit button. Therefore, you won't be able to submit the todo unless you type a todo that is longer than 5 characters. Cool right? 😁
onSubmit Event
const handleSubmit = e => {
e.preventDefault()
if (state.value !== "") {
submit(state.value)
setState({ ...state, value: "", validated: false })
}
}
This method is attached to our form
element and will be fired when someone clicks the Add
button inside the form. In here the submit
method will be passed to the TodoForm
component by App component and you can see that I have extracted it on the fly from props
as a parameter with the help of destructuring.
Creating Todo Element
As we did last time when building TodoForm
component, let's add a folder called Todo
, Todo.js
file and todo.module.css
file.
📦src
┣ 📂components
┃ ┣ 📂Todo
┃ ┃ ┣ 📜Todo.js
┃ ┃ ┗ 📜todo.module.css
┃ ┗ 📂TodoForm
┃ ┃ ┣ 📜TodoForm.js
┃ ┃ ┗ 📜todoForm.module.css
┣ 📜App.js
┣ 📜app.module.css
┣ 📜index.css
┗ 📜index.js
Inside Todo
component we will be needing 4 buttons for each Todo, which are to delete, mark as complete, revert a completed todo and to update. When a Todo is marked as completed, only undo and delete button will be shown and when a Todo is not completed, mark, delete and edit buttons will be shown.
import React, { useState } from "react"
import { Button, List, Input } from "antd"
//styles
import styles from "./todo.module.css"
const Todo = ({ todo, onComplete, onDelete, onUpdate, onUndo }) => {
const { name, isComplete } = todo
const [state, setState] = useState({
currentlyEditing: false,
inputValue: "",
})
const onEdit = () => {
setState({ ...state, currentlyEditing: true, inputValue: name })
}
const onChange = e => {
setState({
...state,
inputValue: e.target.value,
})
}
const onLocalUpdate = () => {
setState({
...state,
currentlyEditing: false,
})
onUpdate(state.inputValue)
}
return (
<List.Item>
<List.Item.Meta
title={
state.currentlyEditing ? (
<Input
className={styles.input}
type="text"
value={state.inputValue}
onChange={onChange}
/>
) : (
<p className={`${styles.text} ${isComplete ? styles.cut : ""}`}>
{name}
</p>
)
}
/>
<Button.Group>
{state.currentlyEditing ? (
<Button
className={styles.editConfirm}
icon="check"
onClick={state.currentlyEditing ? onLocalUpdate : onComplete}
disabled={state.inputValue.length <= 5}
/>
) : !isComplete ? (
<>
<Button onClick={onComplete} icon="check" />
<Button onClick={onEdit} icon="edit" />
<Button onClick={onDelete} icon="delete" />
</>
) : (
<>
<Button onClick={onUndo} icon="undo" />
<Button onClick={onDelete} icon="delete" />
</>
)}
</Button.Group>
</List.Item>
)
}
export default Todo
This component is also implemented as a functional component with the help of React Hooks and different buttons are displayed according to the isComplete
property that each todo has got. When the edit button is clicked currentlyEditing
value is set to true
inside onEdit
and the static text will become an Input element and will give you the ability to edit the text. Changes are recorded with the help of React Hooks implemented inside handleChange
method. onComplete
, onDelete
, undo
methods will be implemented inside the App component.
Finalizing App Component's Methods
App component is a class-based component and it's the largest in this project, it's because it holds the main state of the application plus has so many methods inside to handle various changes happening.
import React, { Component } from "react"
import randomId from "random-id"
import produce from "immer"
import { Helmet } from "react-helmet"
import { List, Card } from "antd"
//custom components
import Todo from "./components/Todo/Todo"
import TodoForm from "./components/TodoForm/TodoForm"
import { Button } from "antd"
//styles
import styles from "./app.module.css"
class App extends Component {
constructor(props) {
super(props)
this.onComplete = this.onComplete.bind(this)
this.onDelete = this.onDelete.bind(this)
this.addTodo = this.addTodo.bind(this)
this.onUpdate = this.onUpdate.bind(this)
this.clearAll = this.clearAll.bind(this)
this.onUndo = this.onUndo.bind(this)
this.storeTodos = this.storeTodos.bind(this)
}
state = {
todos: [],
}
componentDidMount() {
let existingTodos = JSON.parse(localStorage.getItem("todos"))
let fetchededTodos = existingTodos !== null ? existingTodos : []
this.setState({ todos: fetchededTodos })
}
addTodo(todo) {
const newTodos = [
{ name: todo, isComplete: false, id: randomId() },
...this.state.todos,
]
this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
}
onDelete(index) {
if (window.confirm(`Are you sure that you want to delete it?`)) {
let newTodos = [...this.state.todos]
newTodos.splice(index, 1)
this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
}
}
onComplete(index) {
let newTodos = [...this.state.todos]
newTodos[index].isComplete = true
this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
}
onUpdate(value, index) {
const newTodos = [...this.state.todos]
newTodos[index].name = value
this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
}
onUndo(index) {
let newTodos = [...this.state.todos]
newTodos[index].isComplete = false
this.setState(newTodos, () => this.storeTodos(newTodos))
}
storeTodos(newTodos) {
localStorage.setItem("todos", JSON.stringify(newTodos))
}
clearAll() {
if (
window.confirm(
`Are you sure that you want to clear all todos? This can't be undo later!`
)
) {
this.setState({ todos: [] }, () => this.storeTodos([]))
}
}
render() {
return (
<>
<Helmet>
<meta charSet="utf-8" />
<title>My Todos</title>
</Helmet>
<Card
className={styles.app}
title="My Todos"
extra={
<Button
onClick={this.clearAll}
icon="delete"
type="danger"
disabled={this.state.todos.length === 0 ? true : false}
>
Clear All
</Button>
}
>
<TodoForm submit={todo => this.addTodo(todo)} />
<List
dataSource={this.state.todos}
renderItem={(item, index) => (
<Todo
key={item.id}
todo={item}
onComplete={() => this.onComplete(index)}
onDelete={() => this.onDelete(index)}
onUpdate={value => this.onUpdate(value, index)}
onUndo={() => this.onUndo(index)}
/>
)}
/>
</Card>
</>
)
}
}
export default App
Lets go method by method,
addTodo Method
addTodo(todo) {
const newTodos = [
{ name: todo, isComplete: false, id: randomId() },
...this.state.todos,
]
this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
}
addTodo
method is connected with TodoForm
element fired when someone submits the form with a todo. The todo is passed as an argument to this function and it will be prepended to the current list of todos stored as an array.
onComplete Method
onComplete(index) {
let newTodos = [...this.state.todos]
newTodos[index].isComplete = true
this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
}
This method will be fired when someone clicks on the check button that we implemented inside Todo
component, and all these methods are passed to Todo
component with props.
When this method is fired, the isComplete
property of the selected todo will be set to true
and the selected todo is identified with the help of index of the todos array and another thing is that here I have used spread operator in order to clone the current todo, because we should not directly manipulate the state.
onDelete Method
onDelete(index) {
if (window.confirm(`Are you sure that you want to delete it?`)) {
let newTodos = [...this.state.todos]
newTodos.splice(index, 1)
this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
}
}
onDelete
method is fired when someone clicks on the delete button and It will remove the selected todo from the app. Array splice
method has been used to delete one item from the todos array.
onUpdate Method
onUpdate(value, index) {
const newTodos = [...this.state.todos]
newTodos[index].name = value
this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
}
onUndo Method
onUndo(index) {
let newTodos = [...this.state.todos]
newTodos[index].isComplete = false
this.setState(newTodos, () => this.storeTodos(newTodos))
}
When this method is executed, what is does it that it sets the isComplete
property of the selected todo to true
and regular setState
method of React is used to set updated todos as current todos.
clearAll Method
clearAll() {
if (window.confirm(`Are you sure that you want to clear all todos? This can't be undo later!`)) {
this.setState({ todos: [] }, () => this.storeTodos([]))
}
}
Well, This method we give the user the ability to clear all todos at once. When someone clicks the Clear All button they will be prompted with a confirmation popup and if they confirm it an empty array will be set as the sate which removes all recorded todos.
Permanently Storing Todos
We have reached to the end of the application, now all we have to do is to store our todos someone permanent, because now if someone reloads the page after storing some todos, all of them will be gone. The component state is not persistent. That is why we have to use databases to store our data.
As this is a small project and we don't have a huge amount of data, we can use the localstorage
which comes inbuilt to all browsers. We can identify it as a simple little database that is available inside browsers.
So, what we do here is getting the previously stored items when App component is rendred and updating it each time when our sate changes to match it to the current state.
After passing the state to the setState
method, we can pass a callback function to runs when it completes setting the state. That's a good place to store our state in localstorage.
storeTodos(newTodos) {
localStorage.setItem("todos", JSON.stringify(newTodos))
}
In our application, you can see that I have implemented a function named storeTodos
, and it's passed as a callback to all setState methods with the new state. setItem
method can be used to store data into localStorage
and getItem
can be used to fetch stored Items. We can only store key-value pairs inside local storage and that's the reason that I have stringify
our state array before storing.
All stored todos are fetched inside componentDidMount
method.
componentDidMount() {
let existingTodos = JSON.parse(localStorage.getItem("todos"));
let fetchededTodos = existingTodos !== null ? existingTodos : [];
this.setState({ todos: fetchededTodos });
}
As we get stringify
version of our todos we must parse
it to be a real Array
before setting as the state.
Conclusion
I guess that you learned how to build a simple yet powerful todo app with the help of React, React Hooks and Ant Design UI Library. Fell free to comment if you have any questions. Good luck and happy coding until I meet you guys with the next article. 👋 👋 👋