Let’s create the same React CRUD TodoList application with TypeScript. We’ll include TypeScript interfaces and types for better type checking and code reliability.
Step-by-Step Guide for TypeScript
1. Setting Up the Project
First, set up your React project using Create React App with TypeScript.
npx create-react-app todo-app --template typescript
cd todo-app
Install necessary dependencies:
npm install @reduxjs/toolkit react-redux axios
2. Setting Up Redux Toolkit
Configuring the Redux Store
Create a store.ts
file to configure the Redux store.
// src/store.ts
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from './todoSlice';
// Function to save state to LocalStorage
const saveToLocalStorage = (state: any) => {
try {
const serializedState = JSON.stringify(state);
localStorage.setItem('todos', serializedState);
} catch (e) {
console.warn(e);
}
};
// Function to load state from LocalStorage
const loadFromLocalStorage = () => {
try {
const serializedState = localStorage.getItem('todos');
return serializedState ? JSON.parse(serializedState) : undefined;
} catch (e) {
console.warn(e);
return undefined;
}
};
const store = configureStore({
reducer: {
todos: todoReducer, // Register the todo reducer under the 'todos' key
},
preloadedState: loadFromLocalStorage(), // Load state from LocalStorage
});
store.subscribe(() => saveToLocalStorage(store.getState()));
export default store;
3. Creating the Todo Slice
Create a todoSlice.ts
file to manage the state of the TodoList.
// src/todoSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// Define the interface for a Todo
interface Todo {
id: number;
text: string;
completed: boolean;
}
// Define the initial state type
type TodosState = Todo[];
// Define the initial state
const initialState: TodosState = [];
// Create a slice for todos
const todoSlice = createSlice({
name: 'todos', // Name of the slice
initialState, // Initial state of the slice
reducers: {
// Reducer to add a new todo
addTodo: (state, action: PayloadAction<Todo>) => {
state.push(action.payload);
},
// Reducer to toggle the completed state of a todo
toggleTodo: (state, action: PayloadAction<number>) => {
const todo = state.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
// Reducer to edit an existing todo
editTodo: (state, action: PayloadAction<{ id: number; text: string }>) => {
const todo = state.find(todo => todo.id === action.payload.id);
if (todo) {
todo.text = action.payload.text;
}
},
// Reducer to delete a todo
deleteTodo: (state, action: PayloadAction<number>) => {
return state.filter(todo => todo.id !== action.payload);
},
},
});
// Export the action creators generated by createSlice
export const { addTodo, toggleTodo, editTodo, deleteTodo } = todoSlice.actions;
// Export the reducer to be used in the store
export default todoSlice.reducer;
4. Creating the TodoList Component
Create a TodoList.tsx
component to display the list of todos.
// src/components/TodoList.tsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import axios from 'axios';
import { RootState } from '../store';
import { addTodo, toggleTodo, deleteTodo } from '../todoSlice';
import './TodoList.css';
const TodoList: React.FC = () => {
const todos = useSelector((state: RootState) => state.todos); // Get todos from Redux state
const dispatch = useDispatch(); // Get dispatch function
useEffect(() => {
// Fetch todos from JSON server
axios.get('http://localhost:3001/todos')
.then(response => {
response.data.forEach((todo: Todo) => {
dispatch(addTodo({
id: todo.id,
text: todo.text,
completed: todo.completed,
}));
});
});
}, [dispatch]);
return (
<ul>
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
/>
{todo.text}
<button onClick={() => dispatch(deleteTodo(todo.id))}>Delete</button>
</li>
))}
</ul>
);
};
export default TodoList;
Explanation
Importing Libraries and Components:
React
anduseEffect
are imported from React for component creation and side effects.useSelector
anduseDispatch
are imported fromreact-redux
to interact with the Redux store.axios
is imported for making HTTP requests.addTodo
,toggleTodo
, anddeleteTodo
are imported fromtodoSlice
for dispatching actions.TodoList.css
is imported for styling.
Component Definition:
- The
TodoList
component fetches and displays todos.
- The
State and Dispatch:
useSelector
is used to access the current state of todos.useDispatch
is used to dispatch actions.
Fetching Todos:
useEffect
is used to fetch todos from the JSON server when the component mounts. The todos are added to the Redux store using theaddTodo
action.
Rendering Todos:
- The component maps over the
todos
array to render each todo item. Each item includes a checkbox to toggle its completion state and a button to delete the todo.
- The component maps over the
5. Creating the AddTodo Component
Create an AddTodo.tsx
component for adding new todos.
// src/components/AddTodo.tsx
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from '../todoSlice';
const AddTodo: React.FC = () => {
const [text, setText] = useState(''); // Local state for new todo text
const dispatch = useDispatch(); // Get dispatch function
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); // Prevent default form submission behavior
dispatch(addTodo({
id: Date.now(), // Generate unique ID for new todo
text,
completed: false,
}));
setText(''); // Clear input field after adding todo
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add Todo</button>
</form>
);
};
export default AddTodo;
Explanation
Importing Libraries:
React
anduseState
are imported from React for component creation and local state management.useDispatch
is imported fromreact-redux
to interact with the Redux store.addTodo
is imported fromtodoSlice
for dispatching the action.
Component Definition:
- The
AddTodo
component is a form for adding new todos.
- The
Local State:
useState
is used to manage the local state for the input text.
Dispatching Action:
- When the form is submitted,
handleSubmit
is called. This function prevents the default form submission behavior, dispatches theaddTodo
action with the new todo, and clears the input field.
- When the form is submitted,
6. Creating the EditTodo Component
Create an EditTodo.tsx
component for editing existing todos.
// src/components/EditTodo.tsx
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { RootState } from '../store';
import { editTodo } from '../todoSlice';
const EditTodo: React.FC = () => {
const { id } = useParams<{ id: string }>();
const dispatch = useDispatch();
const todo = useSelector((state: RootState) =>
state.todos.find(todo => todo.id === parseInt(id))
);
const [text, setText] = useState(todo ? todo.text : '');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
dispatch(editTodo({
id: parseInt(id),
text,
}));
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Edit Todo</button>
</form>
);
};
export default EditTodo;
Explanation
Importing Libraries:
React
anduseState
are imported from React for component creation and local state management.useDispatch
anduseSelector
are imported fromreact-redux
to interact with the Redux store.useParams
is imported fromreact-router-dom
to access URL parameters.editTodo
is imported fromtodoSlice
for dispatching the action.
Component Definition:
- The
EditTodo
component is a form for editing an existing todo.
- The
Fetching the Todo:
useSelector
is used to find the specific todo from the Redux state that matches theid
provided in the URL parameters (useParams
).
Local State:
useState
is used to manage the local state for the input text, initialized with the current text of the todo to be edited.
Dispatching Action:
- When the form is submitted,
handleSubmit
is called. This function prevents the default form submission behavior, dispatches theeditTodo
action with the updated todo, and updates the Redux state.
- When the form is submitted,
7. Adding CSS for Styling
Create a TodoList.css
file for modern CSS styling.
/* src/components/TodoList.css */
/* Style the list */
ul {
list-style-type: none;
padding: 0;
}
/* Style list items */
li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid #ccc;
}
/* Style completed todos */
li.completed {
text-decoration: line-through;
color: #999;
}
/* Style the delete button */
button {
background-color: #f44336;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
/* Change button color on hover */
button:hover {
background-color: #d32f2f;
}
8. Setting Up JSON Server
Set up a JSON server to handle backend operations.
Install JSON server globally:
npm install -g json-server
Create a db.json
file with some initial data.
// db.json
{
“todos”: [
{ “id”: 1, “text”: “Learn React”, “completed”: false },
{ “id”: 2, “text”: “Learn Redux”, “completed”: false }
]
}
Start the JSON server:
json-server --watch db.json --port 3001
9. Integrating JSON Server with Axios
Fetch data from the JSON server in your components using Axios.
// src/components/TodoList.tsx
import React, { useEffect } from 'react';
import axios from 'axios';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../store';
import { addTodo, toggleTodo, deleteTodo } from '../todoSlice';
import './TodoList.css';
const TodoList: React.FC = () => {
const todos = useSelector((state: RootState) => state.todos);
const dispatch = useDispatch();
useEffect(() => {
axios.get('http://localhost:3001/todos')
.then(response => {
response.data.forEach((todo: Todo) => {
dispatch(addTodo({
id: todo.id,
text: todo.text,
completed: todo.completed,
}));
});
});
}, [dispatch]);
return (
<ul>
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
/>
{todo.text}
<button onClick={() => dispatch(deleteTodo(todo.id))}>Delete</button>
</li>
))}
</ul>
);
};
export default TodoList;
Explanation
Importing Libraries and Components:
React
anduseEffect
are imported from React for component creation and side effects.axios
is imported for making HTTP requests.useSelector
anduseDispatch
are imported fromreact-redux
to interact with the Redux store.addTodo
,toggleTodo
, anddeleteTodo
are imported fromtodoSlice
for dispatching actions.
Component Definition:
- The
TodoList
component fetches and displays todos.
- The
State and Dispatch:
useSelector
is used to access the current state of todos.useDispatch
is used to dispatch actions.
Fetching Todos:
useEffect
is used to fetch todos from the JSON server when the component mounts. The todos are added to the Redux store using theaddTodo
action.
Rendering Todos:
- The component maps over the
todos
array to render each todo item. Each item includes a checkbox to toggle its completion state and a button to delete the todo.
- The component maps over the
Conclusion
Congratulations! You’ve built a modern React CRUD TodoList application with TypeScript, Redux Toolkit, LocalStorage, and JSON server. Your TodoList is fully functional and styled with modern CSS, providing a sleek user experience.
EXPLANATION OF TYPESCRIPT IN REDUX
Using TypeScript with Redux helps enhance the robustness and maintainability of your code by adding type safety. This means that the TypeScript compiler can catch potential errors and provide better tooling support (like autocomplete and type checking). Let’s break down the use of TypeScript in Redux step by step.
1. Defining State Types
Using TypeScript, you can define the shape of your state, ensuring that state objects adhere to the specified structure.
Example
// Define the interface for a Todo
interface Todo {
id: number;
text: string;
completed: boolean;
}
// Define the initial state type
type TodosState = Todo[];
2. Defining Action Types
With TypeScript, you can define action payload types, which helps in enforcing the correct structure of actions dispatched in your application.
Example
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// Create a slice for todos
const todoSlice = createSlice({
name: 'todos',
initialState: [] as TodosState,
reducers: {
addTodo: (state, action: PayloadAction<Todo>) => {
state.push(action.payload);
},
toggleTodo: (state, action: PayloadAction<number>) => {
const todo = state.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
editTodo: (state, action: PayloadAction<{ id: number; text: string }>) => {
const todo = state.find(todo => todo.id === action.payload.id);
if (todo) {
todo.text = action.payload.text;
}
},
deleteTodo: (state, action: PayloadAction<number>) => {
return state.filter(todo => todo.id !== action.payload);
},
},
});
export const { addTodo, toggleTodo, editTodo, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;
3. Configuring the Store
TypeScript helps to define the type of the overall state, allowing better integration with the rest of your application.
Example
// src/store.ts
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from './todoSlice';
const store = configureStore({
reducer: {
todos: todoReducer,
},
// Add middleware here if needed
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
4. Using Selectors and Dispatch in Components
TypeScript ensures that you correctly use state and dispatch functions, reducing runtime errors.
Example
// src/components/TodoList.tsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import axios from 'axios';
import { RootState, AppDispatch } from '../store';
import { addTodo, toggleTodo, deleteTodo } from '../todoSlice';
import './TodoList.css';
const TodoList: React.FC = () => {
const todos = useSelector((state: RootState) => state.todos);
const dispatch: AppDispatch = useDispatch();
useEffect(() => {
axios.get('http://localhost:3001/todos')
.then(response => {
response.data.forEach((todo: Todo) => {
dispatch(addTodo(todo));
});
});
}, [dispatch]);
return (
<ul>
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
/>
{todo.text}
<button onClick={() => dispatch(deleteTodo(todo.id))}>Delete</button>
</li>
))}
</ul>
);
};
export default TodoList;
Benefits of Using TypeScript with Redux
Type Safety:
- Ensures that the state and actions adhere to the defined types, reducing the chances of runtime errors.
- Helps catch type-related errors during compile-time.
Improved Developer Experience:
- Enhances the development process with better tooling, such as autocomplete and inline documentation.
- Makes refactoring easier and safer by ensuring type consistency across the application.
Clearer Code:
- Makes the codebase more readable and understandable by explicitly defining the shape and types of state and actions.
- Reduces ambiguity, making it easier for new developers to understand the code.
Better Integration with IDEs:
- Offers better integration with IDEs, providing features like code navigation, refactoring tools, and inline error checking.
Welcome to DevTechTutor.com, your ultimate resource for mastering web development and technology! Whether you're a beginner eager to dive into coding or an experienced developer looking to sharpen your skills, DevTechTutor.com is here to guide you every step of the way. Our mission is to make learning web development accessible, engaging, and effective.