Weekend Project: Building a Complete React Application

Applying Hooks, State Management, and Forms with a Structured Problem-Solving Approach

Introduction

In this weekend project, we'll build a complete React application that brings together everything we've learned about hooks, state management, and forms. To structure our development process, we'll apply George Polya's famous 4-step problem-solving procedure:

  1. Understand the Problem: Define requirements and clarify what we're building
  2. Devise a Plan: Design the architecture and component structure
  3. Execute the Plan: Implement the application step by step
  4. Review/Extend: Test, refine, and enhance the application

We'll be building a Task Management Application that allows users to:

This project will give you hands-on experience with React's most important concepts and prepare you for real-world development challenges.

Step 1: Understand the Problem

The first step in Polya's method is to thoroughly understand what we're trying to build. Let's define our requirements and clarify the scope of our application.

Application Requirements

flowchart TD A[Task Management App] --> B[User Features] A --> C[Technical Requirements] B --> B1[Task CRUD Operations] B --> B2[Project Organization] B --> B3[Task Filtering & Sorting] B --> B4[Statistics Dashboard] B --> B5[User Preferences] C --> C1[React Hooks Architecture] C --> C2[Complex State Management] C --> C3[Form Validation] C --> C4[Data Persistence] C --> C5[Responsive Design] style A fill:#f9f,stroke:#333,stroke-width:2px

User Stories

Data Models


// Task Model
{
  id: string,          // Unique identifier
  title: string,       // Task title
  description: string, // Optional detailed description
  projectId: string,   // Reference to parent project
  status: enum,        // 'todo', 'in-progress', 'completed'
  priority: enum,      // 'low', 'medium', 'high'
  dueDate: Date,       // Optional due date
  createdAt: Date,     // Creation timestamp
  updatedAt: Date,     // Last update timestamp
  tags: string[]       // Optional tags for categorization
}

// Project Model
{
  id: string,          // Unique identifier
  name: string,        // Project name
  description: string, // Optional project description
  color: string,       // Color identifier for UI
  createdAt: Date,     // Creation timestamp
}

// User Preferences Model
{
  theme: string,       // 'light', 'dark', 'system'
  defaultView: string, // 'board', 'list', 'calendar'
  defaultFilter: object, // Default filter settings
  defaultSort: object  // Default sort settings
}
      

Key React Concepts to Apply

Before moving forward, we need to ensure we understand all aspects of the problem:

Answering these questions gives us a solid foundation before we start designing and coding.

Step 2: Devise a Plan

Now that we understand what we're building, let's devise a plan for our application architecture and development process.

Application Architecture

flowchart TD A[App Component] --> B[Context Providers] B --> C[Layout Components] C --> D[Navigation] C --> E[Main Content Area] C --> F[Modals/Dialogs] E --> G[Dashboard View] E --> H[Project View] E --> I[Task Detail View] E --> J[Settings View] G --> G1[Statistics] G --> G2[Recent Tasks] H --> H1[Project Header] H --> H2[Task List/Board] H --> H3[Project Actions] H2 --> K[Task Components] K --> K1[Task Card] K --> K2[Task Form] K --> K3[Task Filters] style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#33a,stroke-width:2px

State Management Strategy

For an application of this complexity, we'll implement a layered state management approach:

  1. Global Application State: Context API with useReducer
    • Projects and tasks data
    • User preferences
    • UI state (current view, modals, etc.)
  2. Component-Level State: useState for local component state
    • Form input values
    • UI interactions (hover states, expanded/collapsed, etc.)
    • Temporary data (unsaved changes)
  3. Derived State: useMemo and useCallback for computed values
    • Filtered task lists
    • Statistics and aggregations
    • Memoized callback functions

Custom Hooks Plan

We'll create several custom hooks to encapsulate reusable logic:

Development Process

We'll break down the implementation into manageable steps:

  1. Setup Project: Initialize a React application and install dependencies
  2. Create Base Components: Implement layout and container components
  3. Implement State Management: Set up context and reducers
  4. Build Custom Hooks: Develop reusable logic
  5. Create Core Features: Implement CRUD operations for tasks and projects
  6. Add Filtering & Sorting: Implement search and filtering functionality
  7. Develop Statistics Dashboard: Create data visualization components
  8. Implement User Preferences: Add theme switching and preference settings
  9. Polish UI/UX: Refine the interface and interactions
  10. Test & Debug: Thoroughly test all features and fix issues

Component Structure


src/
├── components/
│   ├── layout/
│   │   ├── Header.js
│   │   ├── Sidebar.js
│   │   ├── MainContent.js
│   │   └── Layout.js
│   ├── projects/
│   │   ├── ProjectList.js
│   │   ├── ProjectCard.js
│   │   ├── ProjectForm.js
│   │   └── ProjectDetails.js
│   ├── tasks/
│   │   ├── TaskList.js
│   │   ├── TaskBoard.js
│   │   ├── TaskCard.js
│   │   ├── TaskForm.js
│   │   └── TaskDetails.js
│   ├── dashboard/
│   │   ├── Dashboard.js
│   │   ├── StatisticsPanel.js
│   │   ├── RecentTasks.js
│   │   └── charts/
│   └── common/
│       ├── Button.js
│       ├── Modal.js
│       ├── Dropdown.js
│       └── ConfirmationDialog.js
├── context/
│   ├── TaskContext.js
│   ├── ProjectContext.js
│   ├── PreferencesContext.js
│   └── UIContext.js
├── hooks/
│   ├── useLocalStorage.js
│   ├── useForm.js
│   ├── useTaskFilters.js
│   ├── useProjectStats.js
│   ├── useConfirmation.js
│   └── useKeyboardShortcut.js
├── reducers/
│   ├── taskReducer.js
│   ├── projectReducer.js
│   ├── preferencesReducer.js
│   └── uiReducer.js
├── utils/
│   ├── dateUtils.js
│   ├── filterUtils.js
│   ├── storageUtils.js
│   └── validationUtils.js
├── pages/
│   ├── HomePage.js
│   ├── ProjectPage.js
│   ├── TaskPage.js
│   └── SettingsPage.js
└── App.js
      

Preliminary UI Mockup

graph TB subgraph "App Layout" Header[Header: Logo, Search, User Menu] Sidebar[Sidebar: Projects, Filters] Main[Main Content Area] Header --- Main Sidebar --- Main end subgraph "Dashboard View" Stats[Statistics Charts] Recent[Recent Tasks] Projects[Project Cards] end subgraph "Project View" PHeader[Project Header] TBoard[Task Board: Todo, In Progress, Completed] PActions[Project Actions] end subgraph "Task Detail" THeader[Task Header: Title, Status] TDetails[Task Details: Description, Due Date] TActions[Task Actions: Edit, Delete] end

With our plan in place, we're ready to move to the implementation phase.

Step 3: Execute the Plan

Now that we have a solid plan, let's implement our application piece by piece.

3.1 Project Setup


// Create a new React application
npx create-react-app task-manager

// Install dependencies
cd task-manager
npm install date-fns uuid react-icons

// Optional dependencies for enhanced UI/features
npm install chart.js react-chartjs-2 react-beautiful-dnd styled-components
      

3.2 Context and Reducers

Let's start by implementing our state management with context and reducers:


// src/context/TaskContext.js
import React, { createContext, useReducer, useContext } from 'react';
import taskReducer from '../reducers/taskReducer';
import useLocalStorage from '../hooks/useLocalStorage';

const TaskContext = createContext();

export const TaskProvider = ({ children }) => {
  // Initialize state from localStorage or with defaults
  const [storedTasks, setStoredTasks] = useLocalStorage('tasks', []);
  
  const [state, dispatch] = useReducer(taskReducer, {
    tasks: storedTasks,
    isLoading: false,
    error: null
  });
  
  // Sync state to localStorage when it changes
  React.useEffect(() => {
    setStoredTasks(state.tasks);
  }, [state.tasks, setStoredTasks]);
  
  return (
    <TaskContext.Provider value={{ ...state, dispatch }}>
      {children}
    </TaskContext.Provider>
  );
};

export const useTasks = () => {
  const context = useContext(TaskContext);
  if (context === undefined) {
    throw new Error('useTasks must be used within a TaskProvider');
  }
  return context;
};
      

// src/reducers/taskReducer.js
import { v4 as uuidv4 } from 'uuid';

export const TASK_ACTIONS = {
  ADD_TASK: 'ADD_TASK',
  UPDATE_TASK: 'UPDATE_TASK',
  DELETE_TASK: 'DELETE_TASK',
  SET_TASKS: 'SET_TASKS',
  SET_LOADING: 'SET_LOADING',
  SET_ERROR: 'SET_ERROR'
};

const taskReducer = (state, action) => {
  switch (action.type) {
    case TASK_ACTIONS.ADD_TASK:
      const newTask = {
        id: uuidv4(),
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        ...action.payload
      };
      return {
        ...state,
        tasks: [...state.tasks, newTask]
      };
      
    case TASK_ACTIONS.UPDATE_TASK:
      return {
        ...state,
        tasks: state.tasks.map(task => 
          task.id === action.payload.id
            ? { ...task, ...action.payload, updatedAt: new Date().toISOString() }
            : task
        )
      };
      
    case TASK_ACTIONS.DELETE_TASK:
      return {
        ...state,
        tasks: state.tasks.filter(task => task.id !== action.payload)
      };
      
    case TASK_ACTIONS.SET_TASKS:
      return {
        ...state,
        tasks: action.payload
      };
      
    case TASK_ACTIONS.SET_LOADING:
      return {
        ...state,
        isLoading: action.payload
      };
      
    case TASK_ACTIONS.SET_ERROR:
      return {
        ...state,
        error: action.payload
      };
      
    default:
      return state;
  }
};

export default taskReducer;
      

In a similar manner, we would implement the ProjectContext and PreferencesContext. Next, let's create our custom hooks:

3.3 Custom Hooks


// src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // Get stored value
  const readValue = () => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  };
  
  // State to store our value
  const [storedValue, setStoredValue] = useState(readValue);
  
  // Return a wrapped version of useState's setter function that
  // persists the new value to localStorage
  const setValue = value => {
    try {
      // Allow value to be a function
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      
      // Save state
      setStoredValue(valueToStore);
      
      // Save to localStorage
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.warn(`Error setting localStorage key "${key}":`, error);
    }
  };
  
  // Listen for changes to this localStorage key in other tabs/windows
  useEffect(() => {
    const handleStorageChange = e => {
      if (e.key === key) {
        setStoredValue(readValue());
      }
    };
    
    window.addEventListener('storage', handleStorageChange);
    return () => window.removeEventListener('storage', handleStorageChange);
  }, [key, readValue]);
  
  return [storedValue, setValue];
}

export default useLocalStorage;
      

// src/hooks/useForm.js
import { useState, useCallback } from 'react';

function useForm(initialValues, validate) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  // Update form field
  const handleChange = useCallback(e => {
    const { name, value, type, checked } = e.target;
    setValues(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
    
    // Clear error when field is edited
    if (errors[name]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[name];
        return newErrors;
      });
    }
  }, [errors]);
  
  // Mark field as touched on blur
  const handleBlur = useCallback(e => {
    const { name } = e.target;
    setTouched(prev => ({
      ...prev,
      [name]: true
    }));
    
    // Validate single field on blur
    if (validate) {
      const fieldErrors = validate({ [name]: values[name] });
      if (fieldErrors[name]) {
        setErrors(prev => ({
          ...prev,
          [name]: fieldErrors[name]
        }));
      }
    }
  }, [values, validate]);
  
  // Reset form to initial values
  const resetForm = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
    setIsSubmitting(false);
  }, [initialValues]);
  
  // Validate all fields
  const validateForm = useCallback(() => {
    if (!validate) return true;
    
    const formErrors = validate(values);
    setErrors(formErrors);
    
    // Form is valid if there are no errors
    return Object.keys(formErrors).length === 0;
  }, [values, validate]);
  
  // Handle form submission
  const handleSubmit = useCallback(async (e, onSubmit) => {
    e.preventDefault();
    
    // Mark all fields as touched
    const allTouched = Object.keys(values).reduce((acc, key) => {
      acc[key] = true;
      return acc;
    }, {});
    setTouched(allTouched);
    
    // Validate form
    const isValid = validateForm();
    
    if (isValid) {
      setIsSubmitting(true);
      
      try {
        await onSubmit(values);
        resetForm();
      } catch (error) {
        setErrors({ form: error.message });
      } finally {
        setIsSubmitting(false);
      }
    }
  }, [values, validateForm, resetForm]);
  
  return {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit,
    resetForm,
    setValues
  };
}

export default useForm;
      

3.4 Task Components

Let's implement some key components for task management:


// src/components/tasks/TaskForm.js
import React from 'react';
import { useTasks } from '../../context/TaskContext';
import { useProjects } from '../../context/ProjectContext';
import useForm from '../../hooks/useForm';
import { TASK_ACTIONS } from '../../reducers/taskReducer';

const TaskForm = ({ task, onClose }) => {
  const { dispatch } = useTasks();
  const { projects } = useProjects();
  
  const initialValues = task
    ? { ...task }
    : {
        title: '',
        description: '',
        status: 'todo',
        priority: 'medium',
        projectId: projects[0]?.id || '',
        dueDate: '',
        tags: []
      };
  
  const validateTask = (values) => {
    const errors = {};
    
    if (!values.title.trim()) {
      errors.title = 'Title is required';
    }
    
    if (!values.projectId) {
      errors.projectId = 'Project is required';
    }
    
    // Add more validation as needed
    
    return errors;
  };
  
  const {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit
  } = useForm(initialValues, validateTask);
  
  const onSubmitTask = (formData) => {
    if (task) {
      // Update existing task
      dispatch({
        type: TASK_ACTIONS.UPDATE_TASK,
        payload: formData
      });
    } else {
      // Create new task
      dispatch({
        type: TASK_ACTIONS.ADD_TASK,
        payload: formData
      });
    }
    
    if (onClose) onClose();
  };
  
  return (
    <form onSubmit={(e) => handleSubmit(e, onSubmitTask)}>
      <div className="form-group">
        <label htmlFor="title">Task Title</label>
        <input
          id="title"
          name="title"
          value={values.title}
          onChange={handleChange}
          onBlur={handleBlur}
          className={touched.title && errors.title ? 'error' : ''}
        />
        {touched.title && errors.title && (
          <div className="error-message">{errors.title}</div>
        )}
      </div>
      
      <div className="form-group">
        <label htmlFor="description">Description</label>
        <textarea
          id="description"
          name="description"
          value={values.description}
          onChange={handleChange}
          rows="4"
        />
      </div>
      
      <div className="form-row">
        <div className="form-group">
          <label htmlFor="projectId">Project</label>
          <select
            id="projectId"
            name="projectId"
            value={values.projectId}
            onChange={handleChange}
            onBlur={handleBlur}
            className={touched.projectId && errors.projectId ? 'error' : ''}
          >
            <option value="">Select Project</option>
            {projects.map(project => (
              <option key={project.id} value={project.id}>
                {project.name}
              </option>
            ))}
          </select>
          {touched.projectId && errors.projectId && (
            <div className="error-message">{errors.projectId}</div>
          )}
        </div>
        
        <div className="form-group">
          <label htmlFor="status">Status</label>
          <select
            id="status"
            name="status"
            value={values.status}
            onChange={handleChange}
          >
            <option value="todo">To Do</option>
            <option value="in-progress">In Progress</option>
            <option value="completed">Completed</option>
          </select>
        </div>
      </div>
      
      <div className="form-row">
        <div className="form-group">
          <label htmlFor="priority">Priority</label>
          <select
            id="priority"
            name="priority"
            value={values.priority}
            onChange={handleChange}
          >
            <option value="low">Low</option>
            <option value="medium">Medium</option>
            <option value="high">High</option>
          </select>
        </div>
        
        <div className="form-group">
          <label htmlFor="dueDate">Due Date</label>
          <input
            id="dueDate"
            name="dueDate"
            type="date"
            value={values.dueDate}
            onChange={handleChange}
          />
        </div>
      </div>
      
      <div className="form-actions">
        <button type="button" onClick={onClose}>
          Cancel
        </button>
        <button type="submit" disabled={isSubmitting}>
          {isSubmitting ? 'Saving...' : task ? 'Update Task' : 'Create Task'}
        </button>
      </div>
    </form>
  );
};

export default TaskForm;
      

// src/components/tasks/TaskList.js
import React, { useMemo } from 'react';
import { useTasks } from '../../context/TaskContext';
import { useProjects } from '../../context/ProjectContext';
import TaskCard from './TaskCard';
import useTaskFilters from '../../hooks/useTaskFilters';

const TaskList = ({ projectId, showFilters = true }) => {
  const { tasks } = useTasks();
  const { projects } = useProjects();
  
  const projectTasks = useMemo(() => {
    return projectId
      ? tasks.filter(task => task.projectId === projectId)
      : tasks;
  }, [tasks, projectId]);
  
  const {
    filters,
    filteredTasks,
    handleFilterChange,
    handleSearchChange,
    handleSortChange,
    resetFilters
  } = useTaskFilters(projectTasks);
  
  // Group tasks by status
  const groupedTasks = useMemo(() => {
    return filteredTasks.reduce((acc, task) => {
      if (!acc[task.status]) {
        acc[task.status] = [];
      }
      acc[task.status].push(task);
      return acc;
    }, {
      'todo': [],
      'in-progress': [],
      'completed': []
    });
  }, [filteredTasks]);
  
  if (tasks.length === 0) {
    return <div className="empty-state">No tasks yet. Create your first task!</div>;
  }
  
  if (projectId && projectTasks.length === 0) {
    return <div className="empty-state">No tasks in this project yet.</div>;
  }
  
  return (
    <div className="task-list-container">
      {showFilters && (
        <div className="task-filters">
          <input
            type="text"
            placeholder="Search tasks..."
            value={filters.search}
            onChange={handleSearchChange}
          />
          
          <select 
            value={filters.status} 
            onChange={(e) => handleFilterChange('status', e.target.value)}
          >
            <option value="">All Statuses</option>
            <option value="todo">To Do</option>
            <option value="in-progress">In Progress</option>
            <option value="completed">Completed</option>
          </select>
          
          <select 
            value={filters.priority} 
            onChange={(e) => handleFilterChange('priority', e.target.value)}
          >
            <option value="">All Priorities</option>
            <option value="low">Low</option>
            <option value="medium">Medium</option>
            <option value="high">High</option>
          </select>
          
          <select 
            value={filters.sort} 
            onChange={handleSortChange}
          >
            <option value="updatedAt_desc">Last Updated</option>
            <option value="createdAt_desc">Newest First</option>
            <option value="createdAt_asc">Oldest First</option>
            <option value="dueDate_asc">Due Date (earliest first)</option>
            <option value="priority_desc">Priority (high to low)</option>
          </select>
          
          <button onClick={resetFilters}>Reset Filters</button>
        </div>
      )}
      
      <div className="task-board">
        {Object.entries(groupedTasks).map(([status, statusTasks]) => (
          <div key={status} className={`task-column ${status}`}>
            <h3 className="column-header">
              {status === 'todo' && 'To Do'}
              {status === 'in-progress' && 'In Progress'}
              {status === 'completed' && 'Completed'}
              <span className="task-count">{statusTasks.length}</span>
            </h3>
            
            <div className="task-cards">
              {statusTasks.map(task => (
                <TaskCard 
                  key={task.id} 
                  task={task}
                  project={projects.find(p => p.id === task.projectId)}
                />
              ))}
              
              {statusTasks.length === 0 && (
                <div className="empty-column">No tasks</div>
              )}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default TaskList;
      

3.5 Statistics Dashboard


// src/components/dashboard/StatisticsPanel.js
import React, { useMemo } from 'react';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
import { Pie } from 'react-chartjs-2';
import { useTasks } from '../../context/TaskContext';
import { format, isBefore, isAfter, addDays } from 'date-fns';

// Register Chart.js components
ChartJS.register(ArcElement, Tooltip, Legend);

const StatisticsPanel = () => {
  const { tasks } = useTasks();
  
  // Calculate statistics
  const stats = useMemo(() => {
    const today = new Date();
    const tomorrow = addDays(today, 1);
    
    const byStatus = {
      todo: tasks.filter(task => task.status === 'todo').length,
      inProgress: tasks.filter(task => task.status === 'in-progress').length,
      completed: tasks.filter(task => task.status === 'completed').length
    };
    
    const byPriority = {
      low: tasks.filter(task => task.priority === 'low').length,
      medium: tasks.filter(task => task.priority === 'medium').length,
      high: tasks.filter(task => task.priority === 'high').length
    };
    
    const overdue = tasks.filter(task => 
      task.status !== 'completed' && 
      task.dueDate && 
      isBefore(new Date(task.dueDate), today)
    ).length;
    
    const dueToday = tasks.filter(task => 
      task.status !== 'completed' && 
      task.dueDate && 
      format(new Date(task.dueDate), 'yyyy-MM-dd') === format(today, 'yyyy-MM-dd')
    ).length;
    
    const dueTomorrow = tasks.filter(task => 
      task.status !== 'completed' && 
      task.dueDate && 
      format(new Date(task.dueDate), 'yyyy-MM-dd') === format(tomorrow, 'yyyy-MM-dd')
    ).length;
    
    return {
      total: tasks.length,
      byStatus,
      byPriority,
      overdue,
      dueToday,
      dueTomorrow
    };
  }, [tasks]);
  
  // Prepare chart data
  const statusData = {
    labels: ['To Do', 'In Progress', 'Completed'],
    datasets: [
      {
        data: [stats.byStatus.todo, stats.byStatus.inProgress, stats.byStatus.completed],
        backgroundColor: ['#ffcccb', '#ffffcc', '#ccffcc'],
        borderColor: ['#ff6666', '#ffff66', '#66ff66'],
        borderWidth: 1
      }
    ]
  };
  
  const priorityData = {
    labels: ['Low', 'Medium', 'High'],
    datasets: [
      {
        data: [stats.byPriority.low, stats.byPriority.medium, stats.byPriority.high],
        backgroundColor: ['#ccccff', '#ffccff', '#ffcccc'],
        borderColor: ['#6666ff', '#ff66ff', '#ff6666'],
        borderWidth: 1
      }
    ]
  };
  
  return (
    <div className="statistics-panel">
      <h2>Task Statistics</h2>
      
      <div className="stats-overview">
        <div className="stat-card">
          <h3>Total Tasks</h3>
          <p className="stat-value">{stats.total}</p>
        </div>
        
        <div className="stat-card overdue">
          <h3>Overdue</h3>
          <p className="stat-value">{stats.overdue}</p>
        </div>
        
        <div className="stat-card due-today">
          <h3>Due Today</h3>
          <p className="stat-value">{stats.dueToday}</p>
        </div>
        
        <div className="stat-card due-tomorrow">
          <h3>Due Tomorrow</h3>
          <p className="stat-value">{stats.dueTomorrow}</p>
        </div>
      </div>
      
      <div className="stat-charts">
        <div className="chart-container">
          <h3>Tasks by Status</h3>
          <Pie data={statusData} />
        </div>
        
        <div className="chart-container">
          <h3>Tasks by Priority</h3>
          <Pie data={priorityData} />
        </div>
      </div>
    </div>
  );
};

export default StatisticsPanel;
      

3.6 Main Application Component


// src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { TaskProvider } from './context/TaskContext';
import { ProjectProvider } from './context/ProjectContext';
import { PreferencesProvider } from './context/PreferencesContext';
import { UIProvider } from './context/UIContext';
import Layout from './components/layout/Layout';
import HomePage from './pages/HomePage';
import ProjectPage from './pages/ProjectPage';
import TaskPage from './pages/TaskPage';
import SettingsPage from './pages/SettingsPage';
import './App.css';

function App() {
  return (
    <Router>
      <PreferencesProvider>
        <UIProvider>
          <ProjectProvider>
            <TaskProvider>
              <Layout>
                <Routes>
                  <Route path="/" element={<HomePage />} />
                  <Route path="/projects/:projectId" element={<ProjectPage />} />
                  <Route path="/tasks/:taskId" element={<TaskPage />} />
                  <Route path="/settings" element={<SettingsPage />} />
                </Routes>
              </Layout>
            </TaskProvider>
          </ProjectProvider>
        </UIProvider>
      </PreferencesProvider>
    </Router>
  );
}

export default App;
      

Continue implementing the remaining components following a similar pattern. The code samples above illustrate the core architecture and implementation patterns for our application.

As we progress through implementation, we should regularly test individual components and their integration. This approach allows us to identify and fix issues early in the development process.

Step 4: Review and Extend

The final step in Polya's method is to review our work and consider extensions or improvements.

Application Review

After completing the implementation, we should:

  1. Verify Requirements: Ensure all the requirements from Step 1 have been met
    • Task CRUD operations ✓
    • Project organization ✓
    • Filtering and searching ✓
    • Statistics dashboard ✓
    • User preferences ✓
  2. Test Functionality: Perform thorough testing of all features
    • Create/edit/delete tasks and projects
    • Apply filters and search
    • Verify statistics accuracy
    • Test theme switching and preferences
    • Ensure data persistence works correctly
  3. Check Performance: Ensure the application performs well even with a large number of tasks
    • Verify memoization is working correctly
    • Confirm there are no unnecessary re-renders
    • Ensure filters operate efficiently
  4. Evaluate Code Quality: Review the codebase for:
    • Proper component structure and organization
    • Consistent state management patterns
    • Effective custom hook abstractions
    • Comprehensive error handling
    • Code readability and maintainability

Possible Extensions

After completing the core application, consider these potential extensions:

  1. Authentication System
    • Add user registration and login
    • Implement multi-user support
    • Add user roles and permissions
  2. Backend Integration
    • Replace localStorage with a real backend API
    • Implement proper error handling for API requests
    • Add offline support with sync capabilities
  3. Advanced Features
    • Task dependencies and subtasks
    • Recurring tasks
    • Time tracking integration
    • File attachments for tasks
    • Task comments and collaboration
  4. Enhanced UI/UX
    • Drag-and-drop task reordering
    • Keyboard shortcuts
    • Customizable dashboard widgets
    • Additional view options (calendar, timeline)
    • Animations and transitions
  5. Mobile Optimization
    • Responsive design enhancements
    • Touch gestures
    • Progressive Web App (PWA) features

Lessons Learned

Reflect on what you've learned through this project:

This reflection helps consolidate your learning and prepares you for future, more complex projects.

Practical Tips for Development

Development Workflow

  1. Incremental Development: Build and test one feature at a time
  2. Component Storybook: Consider using Storybook.js to develop and test components in isolation
  3. Git Workflow: Use feature branches for different aspects of the application
  4. Commit Often: Make small, focused commits with clear messages
  5. Regular Testing: Test each component as you build it rather than waiting until the end

Debugging Tips

Performance Optimization

Conclusion

In this weekend project, we've built a comprehensive task management application that demonstrates React's power and flexibility. By following George Polya's 4-step problem-solving approach, we've created a structured process for developing complex applications.

We've applied key React concepts including:

This project serves as an excellent foundation that you can extend and customize for your own needs. The patterns and techniques demonstrated here apply to a wide range of React applications beyond task management.

Remember that becoming proficient with React is an iterative process. Each project provides new insights and opportunities to refine your skills. Continue experimenting, learning, and building more complex applications to deepen your understanding of React and its ecosystem.

Further Resources