In this blog, I am going to show you how to create a simple todo application using vanilla JavaScript and Redux. This is a great way to learn the basics of Redux and how to use it in a practical way.
Before Staring
Before we begin, let’s go over some key concepts and terms that will be used throughout this tutorial:
- Actions: An action is a plain javascript object that describes a change in the application’s state. It typically contains a type property that describes the type of action and any additional data that may be needed to make the change.
- Reducers: A reducer is a pure function that takes in the current state and an action, and returns a new state based on the action. It is responsible for updating the state of the application based on the actions it receives.
- Store: The store is the central repository for all of the application’s state. It is created by passing the root reducer to the createStore() method provided by redux.
- Dispatch: Dispatching an action is the process of sending an action to the store so that it can be handled by the reducers and the state can be updated.
Getting Started
First we will generate a vanilla js project using vite.
Run npm create vite@latest todo-app
and select Vanilla
as Framework.
Next, we need to install the Redux library. We can do this by running the following command in our terminal:
npm install redux --save
Next, we need to create our Redux store. The store holds the state of our application and allows us to dispatch actions that change the state.
Create a file with name store.js
and add following code in it.
// store.js
import { createStore } from "redux"
// Our initial state
const initialState = {
todoArr: [],
}
// Our reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case "ADD_TODO":
// Add a new todo to the list
return {
...state,
todoArr: [...state.todoArr, action.payload],
}
case "REMOVE_TODO":
// Remove a todo from the list
return {
...state,
todoArr: state.todoArr.filter(todo => todo.id !== action.payload),
}
default:
return state
}
}
// Create our store
const store = createStore(reducer)
export default store
In the code above, we have defined our initial state and our reducer. The reducer takes the current state and an action, and returns a new state based on the action. In our case, we have two actions: ADD_TODO and REMOVE_TODO.
Now, let’s create our HTML page and add some basic styles.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo App</title>
</head>
<body>
<header>
<h1 class="header">Todo List</h1>
</header>
<main>
<form class="form">
<div class="form-element">
<label for="title">Title</label>
<input
type="text"
id="title"
name="title"
placeholder="Pickup Groceries"
/>
</div>
<div class="form-element">
<label for="summary">Summary</label>
<textarea
id="summary"
name="summary"
placeholder="Don't forgat to add milk and eggs"
></textarea>
</div>
<button type="submit">Add Task</button>
</form>
<div id="app"></div>
</main>
<script type="module" src="/main.js"></script>
</body>
</html>
In code above we have created a basic form and a app container for todo list.
Now let’s add the logic to add items to our todo list.
// index.js
import "./style.css"
import store from "./store"
// Get the app container
const form = document.querySelector("form")
form.addEventListener("submit", event => {
event.preventDefault()
const formData = new FormData(form)
const dataObj = {}
for (const [key, value] of formData) {
dataObj[key] = value
}
if (!dataObj.title) return
store.dispatch({
type: "ADD_TODO",
payload: dataObj,
})
form.reset()
})
store.subscribe(() => {
generateTodoList(store.getState().todoArr)
})
const app = document.querySelector("#app")
const listEl = document.createElement("ol")
listEl.classList.add("todo-list")
listEl.addEventListener("click", e => {
e.preventDefault()
const action = e.target.dataset.action.toUpperCase()
const index = parseInt(e.target.dataset.index)
store.dispatch({
type: `${action}_TODO`,
payload: index,
})
})
const generateTodoList = list => {
const htmlList = list.map(
(todo, i) =>
`
<li class="todo-list-item" >
${
todo.complete
? `<div class="checkmark">✔</div>`
: `<button data-action="complete" data-index="${i}" title="Complete Task" class="remove-button">✔</button>`
}
<div class="todo-item">
<p class="todo-title">${todo.title}</p>
${todo.summary ? `<p class="todo-summary">${todo.summary}</p>` : ""}
</div>
<button data-action="remove" data-index="${i}" title="Remove Task" class="remove-button">☓</button>
</li>
`
)
listEl.innerHTML = htmlList.join("")
app.append(listEl)
}
In code above we have added an event listener for form submit event. Where we are extracting data from the form and dispatching an event ADD_TODO to update store.
Then we are subscribing to store changes so that we can update todo list whenever store updates.
At last we have created a basic template to render todo item and added an event listener to mark Item as complete or Remove the Item.
Then we will add some css to style the app.
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #1a202c;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
h1 {
font-size: 2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #0c6c33;
cursor: pointer;
transition: background-color 0.25s;
}
button:hover {
background-color: #179848;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #1a202c;
background-color: #ffffff;
}
button {
background-color: #f9f9f9;
}
.todo-item {
background-color: #1a202c;
color: rgba(255, 255, 255, 0.87);
}
}
.header {
text-align: center;
margin-top: 2rem;
}
.form {
margin: auto;
width: 20rem;
}
.form-element {
display: flex;
flex-direction: column;
margin-bottom: 1.25rem;
}
label {
margin-bottom: 0.75rem;
}
input,
textarea {
padding: 0.75rem 1rem;
border-radius: 0.25rem;
border: none;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.todo-list {
margin: 20px 0;
padding: 0;
width: 100%;
max-width: 40rem;
list-style: none;
}
.todo-list-item {
display: flex;
align-items: self-start;
gap: 1rem;
padding: 1rem;
margin-bottom: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, 0.87);
}
.todo-item {
flex: 1;
display: flex;
flex-direction: column;
color: #1a202c;
}
.todo-title {
margin: 0;
font-size: 1.5rem;
}
.todo-summary {
margin: 0;
margin-top: 0.5rem;
}
.remove-button {
padding: 0.25rem;
border-radius: 4px;
line-height: 1;
}
.checkmark {
color: #0c6c33;
}
That’s it. We have created a todo app in Vanilla JS using redux. Next you can add some logic to update a task or store tasks in localstorage for persistance.
Thanks & Keep Coding.