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:
ReactanduseEffectare imported from React for component creation and side effects.useSelectoranduseDispatchare imported fromreact-reduxto interact with the Redux store.axiosis imported for making HTTP requests.addTodo,toggleTodo, anddeleteTodoare imported fromtodoSlicefor dispatching actions.TodoList.cssis imported for styling.
Component Definition:
- The
TodoListcomponent fetches and displays todos.
- The
State and Dispatch:
useSelectoris used to access the current state of todos.useDispatchis used to dispatch actions.
Fetching Todos:
useEffectis used to fetch todos from the JSON server when the component mounts. The todos are added to the Redux store using theaddTodoaction.
Rendering Todos:
- The component maps over the
todosarray 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:
ReactanduseStateare imported from React for component creation and local state management.useDispatchis imported fromreact-reduxto interact with the Redux store.addTodois imported fromtodoSlicefor dispatching the action.
Component Definition:
- The
AddTodocomponent is a form for adding new todos.
- The
Local State:
useStateis used to manage the local state for the input text.
Dispatching Action:
- When the form is submitted,
handleSubmitis called. This function prevents the default form submission behavior, dispatches theaddTodoaction 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:
ReactanduseStateare imported from React for component creation and local state management.useDispatchanduseSelectorare imported fromreact-reduxto interact with the Redux store.useParamsis imported fromreact-router-domto access URL parameters.editTodois imported fromtodoSlicefor dispatching the action.
Component Definition:
- The
EditTodocomponent is a form for editing an existing todo.
- The
Fetching the Todo:
useSelectoris used to find the specific todo from the Redux state that matches theidprovided in the URL parameters (useParams).
Local State:
useStateis 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,
handleSubmitis called. This function prevents the default form submission behavior, dispatches theeditTodoaction 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:
ReactanduseEffectare imported from React for component creation and side effects.axiosis imported for making HTTP requests.useSelectoranduseDispatchare imported fromreact-reduxto interact with the Redux store.addTodo,toggleTodo, anddeleteTodoare imported fromtodoSlicefor dispatching actions.
Component Definition:
- The
TodoListcomponent fetches and displays todos.
- The
State and Dispatch:
useSelectoris used to access the current state of todos.useDispatchis used to dispatch actions.
Fetching Todos:
useEffectis used to fetch todos from the JSON server when the component mounts. The todos are added to the Redux store using theaddTodoaction.
Rendering Todos:
- The component maps over the
todosarray 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.