TypeScript📅 2026-10-08⏱ 11 دقائق قراءة📘 مقال 9 من 10

مشروع React مع TypeScript — تطبيق مهام كامل 2026

مشروع عملي لبناء تطبيق مهام (Todo App) بـ React و TypeScript — خطوة بخطوة مع الكود الكامل والشرح

في المقالات السابقة، تعلمت أساسيات TypeScript — الأنواع، الواجهات، الدوال، الفئات، الأدوية، والأنواع المتقدمة. الآن حان الوقت لتطبيق كل هذه المفاهيم في مشروع حقيقي: تطبيق مهام بـ React و TypeScript.

في هذا الدليل العملي، سنبني تطبيق مهام كاملاً — مع إضافة، حذف، تعديل، حفظ تلقائي، وتصفية المهام، مع الكود الكامل والشرح.

لماذا React + TypeScript؟

  • React: مكتبة واجهات المستخدم الأكثر شعبية.
  • TypeScript: تضمن أمان الأنواع في React.
  • مزيج قوي: معظم المشاريع الحديثة تستخدم هذا المزيج.
  • سوق العمل: مطلوب بشدة في الشركات.

ما سنبنيه

تطبيق مهام بوظائف:

  • إضافة مهمة جديدة.
  • حذف مهمة.
  • تعديل مهمة.
  • تعليم مهمة كمكتملة.
  • تصفية المهام (الكل، النشطة، المكتملة).
  • حفظ تلقائي في localStorage.
  • تصميم جميل مع Tailwind CSS.

هيكل المشروع

سننشئ:

react-todo-typescript/
├── src/
│   ├── components/
│   │   ├── TodoForm.tsx
│   │   ├── TodoItem.tsx
│   │   ├── TodoList.tsx
│   │   └── TodoFilters.tsx
│   ├── types/
│   │   └── todo.ts
│   ├── hooks/
│   │   └── useLocalStorage.ts
│   ├── App.tsx
│   └── main.tsx
├── package.json
├── tsconfig.json
└── vite.config.ts

الخطوة 1: إنشاء المشروع

افتح Terminal، واكتب:

npm create vite@latest react-todo-typescript -- --template react-ts
cd react-todo-typescript
npm install
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

⚠️ ملاحظة: استخدمنا Vite لأنه أسرع وأبسط من Create React App.

الخطوة 2: إعداد Tailwind CSS

افتح tailwind.config.js وعدّله:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

افتح src/index.css واستبدل محتواه:

@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  font-family: 'Cairo', 'Tajawal', sans-serif;
  background: #f9fafb;
  direction: rtl;
}

افتح index.html وأضف خط Cairo:

<!DOCTYPE html>
<html lang="ar" dir="rtl">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;900&display=swap" rel="stylesheet" />
    <title>تطبيق المهام | TypeScript</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

الخطوة 3: تعريف الأنواع (Types)

أنشئ ملف src/types/todo.ts:

// نوع المهمة الواحدة
export interface Todo {
  id: string;
  text: string;
  completed: boolean;
  createdAt: number;
}

// نوع الفلتر
export type FilterType = "all" | "active" | "completed";

// نوع Props لـ TodoForm
export interface TodoFormProps {
  onAdd: (text: string) => void;
}

// نوع Props لـ TodoItem
export interface TodoItemProps {
  todo: Todo;
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
  onEdit: (id: string, newText: string) => void;
}

// نوع Props لـ TodoList
export interface TodoListProps {
  todos: Todo[];
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
  onEdit: (id: string, newText: string) => void;
}

// نوع Props لـ TodoFilters
export interface TodoFiltersProps {
  currentFilter: FilterType;
  onFilterChange: (filter: FilterType) => void;
  activeCount: number;
  completedCount: number;
  onClearCompleted: () => void;
}

شرح:

  • interface Todo: يحدد شكل المهمة.
  • type FilterType: نوع حرفي (Literal Type) للفلاتر.
  • interface ...Props: أنواع الخصائص (Props) لكل مكوّن.

الخطوة 4: Hook مخصص لـ localStorage

أنشئ ملف src/hooks/useLocalStorage.ts:

import { useState, useEffect } from "react";

export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  // قراءة القيمة الأولية
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch (error) {
      console.error("Error reading localStorage:", error);
      return initialValue;
    }
  });

  // حفظ القيمة في localStorage
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error("Error saving to localStorage:", error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue];
}

شرح:

  • <T>: نوع عام (Generic) — يعمل مع أي نوع.
  • useState<T>: حالة بنوع محدد.
  • useEffect: يحفظ في localStorage عند كل تغيير.

الخطوة 5: مكوّن TodoForm

أنشئ ملف src/components/TodoForm.tsx:

import { useState, FormEvent } from "react";
import type { TodoFormProps } from "../types/todo";

export function TodoForm({ onAdd }: TodoFormProps) {
  const [text, setText] = useState<string>("");

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const trimmedText = text.trim();
    if (trimmedText === "") return;
    onAdd(trimmedText);
    setText("");
  };

  return (
    <form onSubmit={handleSubmit} className="flex gap-2 mb-6">
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="أضف مهمة جديدة..."
        className="flex-1 px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-blue-500 focus:outline-none transition text-gray-900"
      />
      <button
        type="submit"
        className="px-6 py-3 bg-blue-600 text-white rounded-xl font-bold hover:bg-blue-700 transition"
      >
        إضافة
      </button>
    </form>
  );
}

شرح:

  • useState<string>: حالة النص مع نوع صريح.
  • FormEvent<HTMLFormElement>: نوع حدث النموذج.
  • TodoFormProps: نوع Props.

الخطوة 6: مكوّن TodoItem

أنشئ ملف src/components/TodoItem.tsx:

import { useState } from "react";
import type { TodoItemProps } from "../types/todo";

export function TodoItem({ todo, onToggle, onDelete, onEdit }: TodoItemProps) {
  const [isEditing, setIsEditing] = useState<boolean>(false);
  const [editText, setEditText] = useState<string>(todo.text);

  const handleSave = () => {
    const trimmed = editText.trim();
    if (trimmed && trimmed !== todo.text) {
      onEdit(todo.id, trimmed);
    }
    setIsEditing(false);
  };

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") handleSave();
    if (e.key === "Escape") {
      setEditText(todo.text);
      setIsEditing(false);
    }
  };

  return (
    <li
      className={`flex items-center gap-3 p-4 rounded-xl transition ${
        todo.completed ? "bg-green-50" : "bg-gray-50 hover:bg-gray-100"
      }`}
    >
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
        className="w-5 h-5 cursor-pointer accent-blue-600 flex-shrink-0"
      />

      {isEditing ? (
        <input
          type="text"
          value={editText}
          onChange={(e) => setEditText(e.target.value)}
          onBlur={handleSave}
          onKeyDown={handleKeyDown}
          autoFocus
          className="flex-1 px-3 py-2 border-2 border-blue-500 rounded-lg focus:outline-none text-gray-900"
        />
      ) : (
        <span
          onDoubleClick={() => setIsEditing(true)}
          className={`flex-1 cursor-pointer ${
            todo.completed ? "line-through text-gray-400" : "text-gray-900"
          }`}
          title="انقر مرتين للتعديل"
        >
          {todo.text}
        </span>
      )}

      <div className="flex gap-2">
        <button
          onClick={() => setIsEditing(!isEditing)}
          className="w-9 h-9 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition"
          title="تعديل"
        >
          ✏️
        </button>
        <button
          onClick={() => onDelete(todo.id)}
          className="w-9 h-9 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition"
          title="حذف"
        >
          🗑️
        </button>
      </div>
    </li>
  );
}

شرح:

  • useState<boolean>: حالة التعديل.
  • KeyboardEvent<HTMLInputElement>: نوع حدث لوحة المفاتيح.
  • onDoubleClick: التعديل بنقرة مزدوجة.

الخطوة 7: مكوّن TodoList

أنشئ ملف src/components/TodoList.tsx:

import type { TodoListProps } from "../types/todo";
import { TodoItem } from "./TodoItem";

export function TodoList({ todos, onToggle, onDelete, onEdit }: TodoListProps) {
  if (todos.length === 0) {
    return (
      <div className="text-center py-12 text-gray-400">
        <span className="text-5xl block mb-3">📭</span>
        <p>لا توجد مهام هنا</p>
      </div>
    );
  }

  return (
    <ul className="space-y-3 mb-6">
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onDelete={onDelete}
          onEdit={onEdit}
        />
      ))}
    </ul>
  );
}

الخطوة 8: مكوّن TodoFilters

أنشئ ملف src/components/TodoFilters.tsx:

import type { FilterType, TodoFiltersProps } from "../types/todo";

export function TodoFilters({
  currentFilter,
  onFilterChange,
  activeCount,
  completedCount,
  onClearCompleted,
}: TodoFiltersProps) {
  const filters: { value: FilterType; label: string }[] = [
    { value: "all", label: "الكل" },
    { value: "active", label: "النشطة" },
    { value: "completed", label: "المكتملة" },
  ];

  return (
    <div className="border-t-2 border-gray-100 pt-4">
      <div className="flex gap-2 mb-4">
        {filters.map((filter) => (
          <button
            key={filter.value}
            onClick={() => onFilterChange(filter.value)}
            className={`flex-1 py-2 rounded-lg font-medium transition ${
              currentFilter === filter.value
                ? "bg-blue-600 text-white"
                : "bg-gray-100 text-gray-700 hover:bg-gray-200"
            }`}
          >
            {filter.label}
          </button>
        ))}
      </div>

      <div className="flex justify-between items-center text-sm text-gray-600">
        <span>
          {activeCount} {activeCount === 1 ? "مهمة متبقية" : "مهام متبقية"}
        </span>
        {completedCount > 0 && (
          <button
            onClick={onClearCompleted}
            className="text-red-600 hover:bg-red-50 px-3 py-1 rounded-lg transition"
          >
            حذف المكتملة ({completedCount})
          </button>
        )}
      </div>
    </div>
  );
}

الخطوة 9: مكوّن App الرئيسي

افتح src/App.tsx واستبدل محتواه:

import { useState, useMemo } from "react";
import { TodoForm } from "./components/TodoForm";
import { TodoList } from "./components/TodoList";
import { TodoFilters } from "./components/TodoFilters";
import { useLocalStorage } from "./hooks/useLocalStorage";
import type { Todo, FilterType } from "./types/todo";

function App() {
  const [todos, setTodos] = useLocalStorage<Todo[]>("todos", []);
  const [filter, setFilter] = useState<FilterType>("all");

  // إضافة مهمة
  const addTodo = (text: string) => {
    const newTodo: Todo = {
      id: crypto.randomUUID(),
      text,
      completed: false,
      createdAt: Date.now(),
    };
    setTodos([newTodo, ...todos]);
  };

  // تعليم كمكتملة
  const toggleTodo = (id: string) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  // حذف مهمة
  const deleteTodo = (id: string) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  // تعديل مهمة
  const editTodo = (id: string, newText: string) => {
    setTodos(
      todos.map((todo) => (todo.id === id ? { ...todo, text: newText } : todo))
    );
  };

  // حذف المكتملة
  const clearCompleted = () => {
    setTodos(todos.filter((todo) => !todo.completed));
  };

  // تصفية المهام
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case "active":
        return todos.filter((t) => !t.completed);
      case "completed":
        return todos.filter((t) => t.completed);
      default:
        return todos;
    }
  }, [todos, filter]);

  const activeCount = todos.filter((t) => !t.completed).length;
  const completedCount = todos.filter((t) => t.completed).length;

  return (
    <div className="min-h-screen bg-gradient-to-br from-blue-500 to-indigo-600 p-4">
      <div className="max-w-2xl mx-auto bg-white rounded-2xl shadow-2xl p-6 mt-8">
        <header className="text-center mb-6">
          <h1 className="text-3xl font-bold text-gray-900 mb-2">
            📝 قائمة المهام
          </h1>
          <p className="text-gray-500 text-sm">TypeScript + React</p>
        </header>

        <TodoForm onAdd={addTodo} />

        <TodoList
          todos={filteredTodos}
          onToggle={toggleTodo}
          onDelete={deleteTodo}
          onEdit={editTodo}
        />

        <TodoFilters
          currentFilter={filter}
          onFilterChange={setFilter}
          activeCount={activeCount}
          completedCount={completedCount}
          onClearCompleted={clearCompleted}
        />
      </div>
    </div>
  );
}

export default App;

شرح:

  • useLocalStorage<Todo[]>: استخدام الـ Hook المخصص.
  • useMemo: تحسين الأداء للفلترة.
  • crypto.randomUUID(): إنشاء ID فريد.
  • نوع Todo: يضمن أن كل مهمة تطابق الشكل المطلوب.

الخطوة 10: تشغيل المشروع

npm run dev

افتح المتصفح على: http://localhost:5173

🎉 مبروك! لقد بنيت تطبيق مهام بـ React و TypeScript!

فهم كيف يعمل TypeScript هنا

1. الأنواع في Props

interface TodoItemProps {
  todo: Todo;
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
  onEdit: (id: string, newText: string) => void;
}

الفائدة: إذا نسيت خاصية أو كتبتها بنوع خاطئ، TypeScript ستعطي خطأ.

2. الأدوية في useLocalStorage

export function useLocalStorage<T>(key: string, initialValue: T): [T, ...]

الفائدة: نفس الـ Hook يعمل مع Todo[]، string، number، أو أي نوع.

3. Union Types في Filter

export type FilterType = "all" | "active" | "completed";

الفائدة: TypeScript تمنع تمرير قيم أخرى غير هذه الثلاثة.

تمارين إضافية

تمرين 1: إضافة أولوية للمهمة

أضف خاصية priority للمهمة (high, medium, low).

الحل:

// في types/todo.ts
export type Priority = "high" | "medium" | "low";

export interface Todo {
  id: string;
  text: string;
  completed: boolean;
  priority: Priority;
  createdAt: number;
}

تمرين 2: إضافة تاريخ استحقاق

أضف dueDate اختياري للمهمة.

الحل:

export interface Todo {
  id: string;
  text: string;
  completed: boolean;
  dueDate?: string;
  createdAt: number;
}

تمرين 3: بحث في المهام

أضف حقل بحث لتصفية المهام.

الحل:

const [searchQuery, setSearchQuery] = useState<string>("");

// في filteredTodos
const filtered = useMemo(() => {
  let result = todos;
  if (searchQuery) {
    result = result.filter((t) =>
      t.text.toLowerCase().includes(searchQuery.toLowerCase())
    );
  }
  switch (filter) {
    case "active":
      return result.filter((t) => !t.completed);
    case "completed":
      return result.filter((t) => t.completed);
    default:
      return result;
  }
}, [todos, filter, searchQuery]);

تمرين 4: إحصائيات

أضف قسم يعرض إحصائيات (عدد المهام، المكتملة، النسبة).

الحل:

const progress = todos.length > 0
  ? Math.round((completedCount / todos.length) * 100)
  : 0;

// في JSX
<div className="bg-gray-50 rounded-xl p-4 mb-4">
  <p>التقدم: {progress}%</p>
  <div className="w-full h-2 bg-gray-200 rounded-full">
    <div
      className="h-full bg-blue-600 rounded-full transition-all"
      style={{ width: `${progress}%` }}
    />
  </div>
</div>

تمرين 5: الوضع الداكن

أضف زراً لتبديل الوضع الداكن.

الحل:

const [darkMode, setDarkMode] = useLocalStorage<boolean>("darkMode", false);

// في JSX
<div className={darkMode ? "dark" : ""}>
  <button onClick={() => setDarkMode(!darkMode)}>
    {darkMode ? "☀️" : "🌙"}
  </button>
</div>

حل المشاكل الشائعة

🔴 المشكلة 1: Type 'X' is not assignable to type 'Y'

السبب: تمرير Props بنوع خاطئ.

<TodoItem todo="نص" />  // ❌ todo يجب أن يكون Todo

الحل: مرر النوع الصحيح.

🔴 المشكلة 2: Property 'X' does not exist on type 'Y'

السبب: الوصول لخاصية غير موجودة.

الحل: تأكد من أن الخاصية موجودة في النوع، أو استخدم ?.

🔴 المشكلة 3: Object is possibly 'null'

السبب: قيمة قد تكون null.

الحل: استخدم ?. (Optional Chaining) أو تحقق من الوجود.

🔴 المشكلة 4: Cannot find module 'X'

السبب: ملف أو مكتبة غير مثبتة.

الحل: تأكد من المسار أو ثبّت المكتبة.

قائمة تحقق نهائية

المهمةالحالة
إنشاء مشروع Vite + React + TS⬜
إعداد Tailwind CSS⬜
تعريف الأنواع (types/todo.ts)⬜
إنشاء useLocalStorage⬜
بناء TodoForm⬜
بناء TodoItem⬜
بناء TodoList و TodoFilters⬜
بناء App الرئيسي⬜
تشغيل المشروع بنجاح⬜
حل تمرين واحد على الأقل⬜

ماذا بعد هذا المشروع؟

🎉 مبروك! لقد أكملت تطبيق React كاملاً بـ TypeScript!

الآن أنت جاهز لـ:

  1. مشروع Node.js — Backend مع TypeScript.
  2. Next.js + TypeScript — إطار عمل كامل.
  3. مكتبات متقدمة — Zustand, React Query.

الخلاصة

في هذا المشروع، طبقت:

  • ✅ الأنواع (Types): Todo, FilterType, ...
  • ✅ الواجهات (Interfaces): TodoItemProps, ...
  • ✅ الأدوية (Generics): useLocalStorage<T>.
  • ✅ Union Types: FilterType.
  • ✅ React Hooks: useState, useEffect, useMemo.
  • ✅ Props مع الأنواع.
  • ✅ الأحداث (Events): FormEvent, KeyboardEvent.

هذا المشروع هو حجر الأساس لبناء تطبيقات React احترافية. احتفظ بالكود، وطور فيه بنفسك!

هل بنيت التطبيق بنجاح؟ شاركنا في التعليقات!

📚 مقالات ذات صلة

TypeScript

مشروع Node.js مع TypeScript — API كامل 2026

مشروع عملي لبناء API كامل بـ Node.js و Express و TypeScript — خطوة بخطوة مع الكود الكامل والشرح

TypeScript

الأنواع المتقدمة في TypeScript — دليل شامل 2026

دليل عملي مفصل للأنواع المتقدمة في TypeScript — Union, Intersection, Conditional, Mapped, Template Literal، مع تمارين وحلول

TypeScript

الأدوية (Generics) في TypeScript — دليل شامل 2026

دليل عملي مفصل للأدوية في TypeScript — الدوال العامة، الواجهات العامة، القيود، مع تمارين وحلول