DEV Community

Paul Robertson
Paul Robertson

Posted on

Build Your First AI-Powered Todo App: React + OpenAI Complete Tutorial

This article contains affiliate links. I may earn a commission at no extra cost to you.


title: "Build Your First AI-Powered Todo App: React + OpenAI Complete Tutorial"
published: true
description: "Learn to build a smart todo app that automatically categorizes tasks and suggests priorities using React and OpenAI's API"
tags: ai, react, tutorial, beginners, openai

cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ai-todo-cover.png

Ever wondered how to add AI superpowers to your web apps? Today we'll build a todo app that doesn't just store your tasksβ€”it understands them. Our AI assistant will automatically categorize tasks, suggest priorities, and even recommend new tasks based on your patterns.

By the end of this tutorial, you'll have a working app deployed online and understand exactly how to integrate OpenAI's API into any React project.

What We're Building

Our smart todo app will:

  • βœ… Handle basic CRUD operations for tasks
  • πŸ€– Auto-categorize tasks (Work, Personal, Health, etc.)
  • πŸ“Š Suggest priority levels (High, Medium, Low)
  • πŸ’‘ Recommend new tasks based on existing ones
  • ⚑ Handle loading states and API errors gracefully

Prerequisites

  • Basic React knowledge (hooks, state management)
  • Node.js installed on your machine
  • An OpenAI API key (get one here)

Step 1: Project Setup

Let's start with a fresh React app:

npx create-react-app ai-todo-app
cd ai-todo-app
npm install axios
npm start
Enter fullscreen mode Exit fullscreen mode

Create a .env file in your project root:

REACT_APP_OPENAI_API_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

⚠️ Important: Never commit API keys to version control. Add .env to your .gitignore.

Step 2: Basic Todo Structure

Replace src/App.js with our foundation:

import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  const [tasks, setTasks] = useState([]);
  const [newTask, setNewTask] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const addTask = async () => {
    if (!newTask.trim()) return;

    setLoading(true);
    setError('');

    try {
      const aiEnhancedTask = await enhanceTaskWithAI(newTask);

      const task = {
        id: Date.now(),
        text: newTask,
        completed: false,
        category: aiEnhancedTask.category,
        priority: aiEnhancedTask.priority,
        createdAt: new Date().toISOString()
      };

      setTasks([...tasks, task]);
      setNewTask('');
    } catch (err) {
      setError('Failed to process task. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  const toggleTask = (id) => {
    setTasks(tasks.map(task => 
      task.id === id ? { ...task, completed: !task.completed } : task
    ));
  };

  const deleteTask = (id) => {
    setTasks(tasks.filter(task => task.id !== id));
  };

  return (
    <div className="App">
      <header className="app-header">
        <h1>πŸ€– AI Todo Assistant</h1>
      </header>

      <main className="app-main">
        <div className="task-input">
          <input
            type="text"
            value={newTask}
            onChange={(e) => setNewTask(e.target.value)}
            placeholder="What needs to be done?"
            onKeyPress={(e) => e.key === 'Enter' && addTask()}
            disabled={loading}
          />
          <button onClick={addTask} disabled={loading || !newTask.trim()}>
            {loading ? 'πŸ€– Thinking...' : 'Add Task'}
          </button>
        </div>

        {error && <div className="error">{error}</div>}

        <TaskList 
          tasks={tasks} 
          onToggle={toggleTask} 
          onDelete={deleteTask} 
        />

        <AIInsights tasks={tasks} />
      </main>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Step 3: OpenAI Integration

Create src/aiService.js to handle our AI logic:

import axios from 'axios';

const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
const API_KEY = process.env.REACT_APP_OPENAI_API_KEY;

const openaiClient = axios.create({
  baseURL: OPENAI_API_URL,
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  }
});

export const enhanceTaskWithAI = async (taskText) => {
  const prompt = `
    Analyze this task and provide category and priority:
    Task: "${taskText}"

    Respond with JSON only:
    {
      "category": "Work|Personal|Health|Shopping|Learning|Other",
      "priority": "High|Medium|Low",
      "reasoning": "brief explanation"
    }
  `;

  try {
    const response = await openaiClient.post('', {
      model: 'gpt-3.5-turbo',
      messages: [{ role: 'user', content: prompt }],
      max_tokens: 150,
      temperature: 0.3
    });

    const aiResponse = response.data.choices[0].message.content;
    return JSON.parse(aiResponse);
  } catch (error) {
    console.error('AI Enhancement failed:', error);
    // Fallback to basic categorization
    return {
      category: 'Other',
      priority: 'Medium',
      reasoning: 'AI unavailable, using defaults'
    };
  }
};

export const generateTaskSuggestions = async (existingTasks) => {
  const taskList = existingTasks.map(t => `- ${t.text} (${t.category})`).join('\n');

  const prompt = `
    Based on these existing tasks, suggest 3 helpful new tasks:
    ${taskList}

    Respond with JSON array:
    [
      {"task": "task description", "category": "category", "priority": "priority"},
      {"task": "task description", "category": "category", "priority": "priority"},
      {"task": "task description", "category": "category", "priority": "priority"}
    ]
  `;

  try {
    const response = await openaiClient.post('', {
      model: 'gpt-3.5-turbo',
      messages: [{ role: 'user', content: prompt }],
      max_tokens: 200,
      temperature: 0.7
    });

    const suggestions = JSON.parse(response.data.choices[0].message.content);
    return suggestions;
  } catch (error) {
    console.error('Suggestion generation failed:', error);
    return [];
  }
};
Enter fullscreen mode Exit fullscreen mode

Step 4: Task Components

Create src/components/TaskList.js:

import React from 'react';

const TaskList = ({ tasks, onToggle, onDelete }) => {
  const getPriorityColor = (priority) => {
    switch (priority) {
      case 'High': return '#ff4757';
      case 'Medium': return '#ffa502';
      case 'Low': return '#2ed573';
      default: return '#747d8c';
    }
  };

  const getCategoryIcon = (category) => {
    const icons = {
      Work: 'πŸ’Ό',
      Personal: '🏠',
      Health: 'πŸ₯',
      Shopping: 'πŸ›’',
      Learning: 'πŸ“š',
      Other: 'πŸ“'
    };
    return icons[category] || 'πŸ“';
  };

  return (
    <div className="task-list">
      {tasks.map(task => (
        <div 
          key={task.id} 
          className={`task-item ${task.completed ? 'completed' : ''}`}
        >
          <div className="task-content">
            <input
              type="checkbox"
              checked={task.completed}
              onChange={() => onToggle(task.id)}
            />
            <span className="task-text">{task.text}</span>
          </div>

          <div className="task-meta">
            <span className="category">
              {getCategoryIcon(task.category)} {task.category}
            </span>
            <span 
              className="priority"
              style={{ color: getPriorityColor(task.priority) }}
            >
              {task.priority}
            </span>
            <button 
              className="delete-btn"
              onClick={() => onDelete(task.id)}
            >
              ❌
            </button>
          </div>
        </div>
      ))}
    </div>
  );
};

export default TaskList;
Enter fullscreen mode Exit fullscreen mode

Create src/components/AIInsights.js:

import React, { useState } from 'react';
import { generateTaskSuggestions } from '../aiService';

const AIInsights = ({ tasks }) => {
  const [suggestions, setSuggestions] = useState([]);
  const [loading, setLoading] = useState(false);

  const getSuggestions = async () => {
    if (tasks.length === 0) return;

    setLoading(true);
    try {
      const newSuggestions = await generateTaskSuggestions(tasks);
      setSuggestions(newSuggestions);
    } catch (error) {
      console.error('Failed to get suggestions:', error);
    } finally {
      setLoading(false);
    }
  };

  const getTaskStats = () => {
    const categories = {};
    const priorities = {};

    tasks.forEach(task => {
      categories[task.category] = (categories[task.category] || 0) + 1;
      priorities[task.priority] = (priorities[task.priority] || 0) + 1;
    });

    return { categories, priorities };
  };

  const { categories, priorities } = getTaskStats();

  return (
    <div className="ai-insights">
      <h3>πŸ“Š AI Insights</h3>

      {tasks.length > 0 && (
        <div className="stats">
          <div className="stat-group">
            <h4>Categories</h4>
            {Object.entries(categories).map(([cat, count]) => (
              <span key={cat} className="stat-item">
                {cat}: {count}
              </span>
            ))}
          </div>

          <div className="stat-group">
            <h4>Priorities</h4>
            {Object.entries(priorities).map(([pri, count]) => (
              <span key={pri} className="stat-item">
                {pri}: {count}
              </span>
            ))}
          </div>
        </div>
      )}

      <button 
        onClick={getSuggestions}
        disabled={loading || tasks.length === 0}
        className="suggestions-btn"
      >
        {loading ? 'πŸ€– Generating...' : 'πŸ’‘ Get AI Suggestions'}
      </button>

      {suggestions.length > 0 && (
        <div className="suggestions">
          <h4>Suggested Tasks:</h4>
          {suggestions.map((suggestion, index) => (
            <div key={index} className="suggestion-item">
              <span>{suggestion.task}</span>
              <small>({suggestion.category} - {suggestion.priority})</small>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

export default AIInsights;
Enter fullscreen mode Exit fullscreen mode

Step 5: Styling

Add this CSS to src/App.css:

.App {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto';
}

.app-header {
  text-align: center;
  margin-bottom: 30px;
}

.task-input {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.task-input input {
  flex: 1;
  padding: 12px;
  border: 2px solid #e1e8ed;
  border-radius: 8px;
  font-size: 16px;
}

.task-input button {
  padding: 12px 24px;
  background: #1da1f2;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
}

.task-input button:disabled {
  background: #aab8c2;
  cursor: not-allowed;
}

.error {
  background: #ffebee;
  color: #c62828;
  padding: 12px;
  border-radius: 8px;
  margin-bottom: 20px;
}

.task-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  border: 1px solid #e1e8ed;
  border-radius: 8px;
  margin-bottom: 10px;
  background: white;
}

.task-item.completed {
  opacity: 0.6;
  text-decoration: line-through;
}

.task-content {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 1;
}

.task-meta {
  display: flex;
  align-items: center;
  gap: 15px;
  font-size: 14px;
}

.category {
  background: #f7f9fa;
  padding: 4px 8px;
  border-radius: 4px;
}

.priority {
  font-weight: 600;
}

.delete-btn {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 16px;
}

.ai-insights {
  margin-top: 40px;
  padding: 20px;
  background: #f8f9fa;
  border-radius: 12px;
}

.stats {
  display: flex;
  gap: 30px;
  margin: 20px 0;
}

.stat-group h4 {
  margin: 0 0 10px 0;
  color: #14171a;
}

.stat-item {
  display: inline-block;
  background: white;
  padding: 4px 8px;
  margin: 2px;
  border-radius: 4px;
  font-size: 12px;
}

.suggestions-btn {
  background: #17bf63;
  color: white;
  border: none;
  padding: 12px 20px;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
}

.suggestions {
  margin-top: 20px;
}

.suggestion-item {
  background: white;
  padding: 12px;
  margin: 8px 0;
  border-radius: 8px;
  border-left: 4px solid #1da1f2;
}

.suggestion-item small {
  color: #657786;
  margin-left: 10px;
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Error Handling & Loading States

Update your src/aiService.js to include better error handling:

// Add this helper function
const handleAPIError = (error) => {
  if (error.response?.status === 429) {
    throw new Error('Rate limit exceeded. Please try again later.');
  } else if (error.response?.status === 401) {
    throw new Error('Invalid API key. Please check your configuration.');
  } else if (error.response?.status >= 500) {
    throw new Error('OpenAI service temporarily unavailable.');
  } else {
    throw new Error('Failed to process request. Please try again.');
  }
};

// Update your enhanceTaskWithAI function
export const enhanceTaskWithAI = async (taskText) => {
  // ... existing code ...

  try {
    const response = await openaiClient.post('', {
      model: 'gpt-3.5-turbo',
      messages: [{ role: 'user', content: prompt }],
      max_tokens: 150,
      temperature: 0.3
    });

    const aiResponse = response.data.choices[0].message.content;
    return JSON.parse(aiResponse);
  } catch (error) {
    handleAPIError(error);
  }
};
Enter fullscreen mode Exit fullscreen mode

Step 7: Deployment

Deploy to Netlify (free and simple):

  1. Build your app:
npm run build
Enter fullscreen mode Exit fullscreen mode
  1. Install Netlify CLI:
npm install -g netlify-cli
Enter fullscreen mode Exit fullscreen mode
  1. Deploy:
netlify deploy --prod --dir=build
Enter fullscreen mode Exit fullscreen mode
  1. Set environment variables in Netlify dashboard:
    • Go to Site settings β†’ Environment variables
    • Add REACT_APP_OPENAI_API_KEY with your API key

Key Takeaways

What works well with AI integration:

  • Text classification and categorization
  • Priority assessment based on context
  • Pattern recognition in user behavior
  • Generating contextual suggestions

Important considerations:

  • Always implement fallbacks for API failures
  • Handle rate limits gracefully
  • Keep API costs in mind (use appropriate token limits)
  • Never expose API keys in client-side code

Performance tips:

  • Cache AI responses when possible
  • Use lower temperature values (0.1-0.3) for consistent categorization
  • Implement request debouncing for real-time features

Next Steps

Your AI-powered todo app is now live! Here are some enhancements to consider:

  • Add user authentication and data persistence
  • Implement smart scheduling suggestions
  • Add voice input with speech-to-text
  • Create custom categories based on user patterns
  • Add deadline predictions based on task complexity

The key to successful AI integration isn't about replacing human decision-makingβ€”it's about augmenting it with intelligent assistance that saves time and provides insights you might miss.

What AI features will you add to your next project? Share your creations in the comments below!


Want to see the complete source code? Check out the GitHub repository and try the live demo.


Tools mentioned:

Top comments (0)