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 احترافية.** احتفظ بالكود، وطور فيه بنفسك!
**هل بنيت التطبيق بنجاح؟ شاركنا في التعليقات!**