TypeScript

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

📅 2026-10-08⏱ 11 دقائق قراءة
في المقالات السابقة، تعلمت أساسيات 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، واكتب: ```bash 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`** وعدّله: ```javascript /** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], }; ``` **افتح `src/index.css`** واستبدل محتواه: ```css @tailwind base; @tailwind components; @tailwind utilities; body { font-family: 'Cairo', 'Tajawal', sans-serif; background: #f9fafb; direction: rtl; } ``` **افتح `index.html`** وأضف خط Cairo: ```html <!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`:** ```typescript // نوع المهمة الواحدة 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`:** ```typescript 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`:** ```typescript 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`:** ```typescript 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`:** ```typescript 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`:** ```typescript 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`** واستبدل محتواه: ```typescript 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: تشغيل المشروع ```bash npm run dev ``` افتح المتصفح على: `http://localhost:5173` **🎉 مبروك! لقد بنيت تطبيق مهام بـ React و TypeScript!** ## فهم كيف يعمل TypeScript هنا ### 1. الأنواع في Props ```typescript interface TodoItemProps { todo: Todo; onToggle: (id: string) => void; onDelete: (id: string) => void; onEdit: (id: string, newText: string) => void; } ``` **الفائدة:** إذا نسيت خاصية أو كتبتها بنوع خاطئ، TypeScript ستعطي خطأ. ### 2. الأدوية في `useLocalStorage` ```typescript export function useLocalStorage<T>(key: string, initialValue: T): [T, ...] ``` **الفائدة:** نفس الـ Hook يعمل مع `Todo[]`، `string`، `number`، أو أي نوع. ### 3. Union Types في Filter ```typescript export type FilterType = "all" | "active" | "completed"; ``` **الفائدة:** TypeScript تمنع تمرير قيم أخرى غير هذه الثلاثة. ## تمارين إضافية ### تمرين 1: إضافة أولوية للمهمة أضف خاصية `priority` للمهمة (high, medium, low). **الحل:** ```typescript // في types/todo.ts export type Priority = "high" | "medium" | "low"; export interface Todo { id: string; text: string; completed: boolean; priority: Priority; createdAt: number; } ``` ### تمرين 2: إضافة تاريخ استحقاق أضف `dueDate` اختياري للمهمة. **الحل:** ```typescript export interface Todo { id: string; text: string; completed: boolean; dueDate?: string; createdAt: number; } ``` ### تمرين 3: بحث في المهام أضف حقل بحث لتصفية المهام. **الحل:** ```typescript 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: إحصائيات أضف قسم يعرض إحصائيات (عدد المهام، المكتملة، النسبة). **الحل:** ```typescript 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: الوضع الداكن أضف زراً لتبديل الوضع الداكن. **الحل:** ```typescript 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 بنوع خاطئ. ```tsx <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'` **السبب:** ملف أو مكتبة غير مثبتة. **الحل:** تأكد من المسار أو ثبّت المكتبة. ## قائمة تحقق نهائية <table> <thead> <tr> <th>المهمة</th> <th>الحالة</th> </tr> </thead> <tbody> <tr> <td>إنشاء مشروع Vite + React + TS</td> <td>⬜</td> </tr> <tr> <td>إعداد Tailwind CSS</td> <td>⬜</td> </tr> <tr> <td>تعريف الأنواع (types/todo.ts)</td> <td>⬜</td> </tr> <tr> <td>إنشاء useLocalStorage</td> <td>⬜</td> </tr> <tr> <td>بناء TodoForm</td> <td>⬜</td> </tr> <tr> <td>بناء TodoItem</td> <td>⬜</td> </tr> <tr> <td>بناء TodoList و TodoFilters</td> <td>⬜</td> </tr> <tr> <td>بناء App الرئيسي</td> <td>⬜</td> </tr> <tr> <td>تشغيل المشروع بنجاح</td> <td>⬜</td> </tr> <tr> <td>حل تمرين واحد على الأقل</td> <td>⬜</td> </tr> </tbody> </table> ## ماذا بعد هذا المشروع؟ 🎉 **مبروك! لقد أكملت تطبيق 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 احترافية.** احتفظ بالكود، وطور فيه بنفسك! **هل بنيت التطبيق بنجاح؟ شاركنا في التعليقات!**