Building an Advanced Task Management App with React, Redux Toolkit, Tailwind CSS, and React Router
In this project, we'll create a Task Management App that will deepen our understanding of React and Redux Toolkit. By the end, you'll be familiar with setting up an app with user authentication, task management, API integration, custom routing with React Router, and styling with Tailwind CSS.
This guide is ideal for anyone who already understands the basics of React and is looking to apply those skills to build a more complex, real-world application.
Project Setup
First, let’s set up our application.
Step 1: Initialize the Project
To begin, create a new React project. You can think of Create React App
like setting up the skeleton of a house—it provides the structure but leaves the specific details for you to build.
npx create-react-app task-manager
cd task-manager
Step 2: Install Dependencies
We'll need several packages to help manage state, handle routing, and add custom styling.
Redux Toolkit: Manages global state (like a central library storing app-wide data).
React Router: Handles page navigation (similar to navigating through different rooms in a house).
Tailwind CSS: Allows for quick and responsive styling using utility classes.
Run this command to install all required packages:
npm install @reduxjs/toolkit react-redux react-router-dom tailwindcss axios
Step 3: Configure Tailwind CSS
Tailwind CSS is like a toolbox with pre-made styling options. Instead of writing custom CSS for each style, you can quickly add styling by applying Tailwind's pre-defined utility classes.
3.1. Set Up Tailwind Configuration
To enable Tailwind, start by creating a configuration file:
npx tailwindcss init
3.2. Tailwind Config File
In tailwind.config.js
, specify where Tailwind should look for files to apply styles. It’s like telling Tailwind where it should “keep an eye out” for components that need styling:
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
3.3. Tailwind Directives
Open src/index.css
and add the following lines to import Tailwind’s styling layers:
@tailwind base;
@tailwind components;
@tailwind utilities;
This makes Tailwind's styling available throughout your app.
Step 4: Set Up the Redux Store
Redux acts as a global storage area for our app's state. Imagine it as a global bank where each component can withdraw or deposit data.
4.1. Create the Store File
Make a directory and file for your Redux store, where you’ll combine all your data slices:
mkdir src/app
touch src/app/store.js
Inside src/app/store.js
, set up the main store with configureStore
, which will hold all the data slices. Here’s the initial setup:
import { configureStore } from '@reduxjs/toolkit';
import authReducer from '../features/auth/authSlice';
import tasksReducer from '../features/tasks/tasksSlice'; // For task management
export const store = configureStore({
reducer: {
auth: authReducer,
tasks: tasksReducer, // Add tasks slice
},
});
Step 5: Create the Authentication Slice
Now, let’s create an auth
slice to handle login and logout. Each slice is like a specific drawer in a filing cabinet, dedicated to storing only related data—in this case, data about our user's login state.
5.1. Create Auth Slice
mkdir src/features/auth
touch src/features/auth/authSlice.js
In src/features/auth/authSlice.js
:
import { createSlice } from '@reduxjs/toolkit';
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null, // Initially, no user is logged in
},
reducers: {
login(state, action) {
state.user = action.payload; // Store user information
},
logout(state) {
state.user = null; // Clear user data when logged out
},
},
});
export const { login, logout } = authSlice.actions;
export default authSlice.reducer;
Step 6: Create the Tasks Slice
Let’s create a tasks
slice for managing our tasks. This slice will handle adding, fetching, and deleting tasks.
6.1. Create Tasks Slice
mkdir src/features/tasks
touch src/features/tasks/tasksSlice.js
In src/features/tasks/tasksSlice.js
, we will manage the state for tasks:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
export const fetchTasks = createAsyncThunk('tasks/fetchTasks', async () => {
const response = await axios.get('https://jsonplaceholder.typicode.com/todos');
return response.data; // Returns the tasks from the API
});
const tasksSlice = createSlice({
name: 'tasks',
initialState: {
tasks: [],
status: 'idle', // idle, loading, succeeded, failed
error: null,
},
reducers: {
addTask(state, action) {
state.tasks.push(action.payload); // Add a new task
},
removeTask(state, action) {
const index = state.tasks.findIndex(task => task.id === action.payload);
if (index !== -1) {
state.tasks.splice(index, 1); // Remove a task by id
}
},
},
extraReducers: (builder) => {
builder
.addCase(fetchTasks.pending, (state) => {
state.status = 'loading'; // Set status to loading
})
.addCase(fetchTasks.fulfilled, (state, action) => {
state.status = 'succeeded'; // Set status to succeeded
state.tasks = action.payload; // Store fetched tasks
})
.addCase(fetchTasks.rejected, (state, action) => {
state.status = 'failed'; // Set status to failed
state.error = action.error.message; // Store the error
});
},
});
export const { addTask, removeTask } = tasksSlice.actions;
export default tasksSlice.reducer;
Step 7: TaskList Component
Now let’s add the main functionality—Task Management. Imagine this feature as a notebook where users can write, track, and organize their tasks.
7.1. Create TaskList Component
Create a new file for the task list component:
touch src/components/TaskList.js
In src/components/TaskList.js
, write the component to display tasks:
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchTasks, removeTask } from '../features/tasks/tasksSlice';
const TaskList = () => {
const dispatch = useDispatch();
const tasks = useSelector((state) => state.tasks.tasks);
const taskStatus = useSelector((state) => state.tasks.status);
const error = useSelector((state) => state.tasks.error);
useEffect(() => {
if (taskStatus === 'idle') {
dispatch(fetchTasks()); // Fetch tasks when the component mounts
}
}, [taskStatus, dispatch]);
const handleDelete = (id) => {
dispatch(removeTask(id)); // Dispatch action to remove task
};
let content;
if (taskStatus === 'loading') {
content = <div>Loading...</div>; // Loading state
} else if (taskStatus === 'succeeded') {
content = tasks.map(task => (
<div key={task.id} className="flex justify-between items-center bg-white p-4 shadow rounded mb-2">
<div>{task.title}</div>
<button onClick={() => handleDelete(task.id)} className="bg-red-500 text-white px-2 py-1 rounded">
Delete
</button>
</div>
));
} else if (taskStatus === 'failed') {
content = <div>{error}</div>; // Error state
}
return (
<div className="p-4">
<h2 className="text-xl font-bold mb-4">Task List</h2>
{content}
</div>
);
};
export default TaskList;
Step 8: Add React Router
With React Router, we can navigate between pages in our app. Think of it as different sections or rooms in a house where each room serves a unique purpose.
8.1. Set Up Routes
Now, configure React Router
in App.js
:
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './app/store';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import TaskList from './components/TaskList';
import Login from './components/Login';
const App = () => {
return (
<Provider store={store}>
<Router>
<
Routes>
<Route path="/" element={<Login />} />
<Route path="/tasks" element={<TaskList />} />
</Routes>
</Router>
</Provider>
);
};
export default App;
Step 9: Create Login Component
Next, let's create a simple login component to simulate user authentication.
9.1. Create Login Component
touch src/components/Login.js
In src/components/Login.js
, we’ll manage user login and logout:
import React from 'react';
import { useDispatch } from 'react-redux';
import { login } from '../features/auth/authSlice';
import { useNavigate } from 'react-router-dom';
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const handleLogin = () => {
const user = { name: 'User' }; // Mock user
dispatch(login(user)); // Dispatch login action
navigate('/tasks'); // Redirect to tasks page
};
return (
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
<h1 className="text-2xl font-bold mb-4">Task Manager Login</h1>
<button onClick={handleLogin} className="bg-blue-500 text-white px-4 py-2 rounded">
Login
</button>
</div>
);
};
export default Login;
Step 10: Add Basic Styling
To give the app a polished look, we can use Tailwind CSS utility classes. For example, the buttons are styled with bg-blue-500
, which applies a blue background color.
Step 11: Run the Application
Finally, let’s run our application and see everything in action. Use the following command to start the development server:
npm run dev
Your app should now be running at http://localhost:5173
. You can log in, view your tasks, and delete them!
Summary
In this tutorial, we built a Task Management App using React, Redux Toolkit, React Router, and Tailwind CSS. We covered:
Setting up the project and installing dependencies.
Creating Redux slices for authentication and task management.
Building components for user login and task display.
Adding routing for seamless navigation between components.
By using Redux for state management and Tailwind CSS for styling, you have learned to create a more complex React application.
Feel free to expand this app further by adding features such as:
Persistent login state using localStorage.
Editing tasks.
Adding categories for tasks.
Implementing user registration.
Final Notes
In coding, just like in life, there’s always room for improvement. Keep experimenting and building to enhance your skills!
"The best way to predict the future is to create it." — Peter Drucker
Feel free to reach out for any questions or clarifications as you work through this project. Happy coding! Brought to by Martin Lubowa X AI
Subscribe to my newsletter
Read articles from Martin Lubowa directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Martin Lubowa
Martin Lubowa
Martin Lubowa is a software engineer passionate about using technology to merge entrepreneurship with education/healthcare sectors in Africa to build resilient and prosperous enterprises. He has been the co-founder and managing director of the Africa Students Support Network (AFRISSUN), a community-based non-organization in Uganda. He has led several charity drives to mobilize food/educational resources for underserved communities.