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 storeIn 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.