Building an Advanced Task Management App with React, Redux Toolkit, Tailwind CSS, and React Router

Martin LubowaMartin Lubowa
Oct 28, 2024·
7 min read

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.

  1. Redux Toolkit: Manages global state (like a central library storing app-wide data).

  2. React Router: Handles page navigation (similar to navigating through different rooms in a house).

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

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