دليلك الشامل: بناء تطبيق إدارة المهام (Todo App) باستخدام React و TypeScript و NodeJS و MongoDB
دليلك الشامل: بناء تطبيق إدارة المهام (Todo App) باستخدام React و TypeScript و NodeJS و MongoDB
في هذا الدليل التفصيلي، سنخوض غمار بناء تطبيق إدارة المهام (Todo App) متكامل من الصفر. سنستخدم TypeScript في كل من جانب الخادم والعميل لضمان قوة التعليمات البرمجية وقابليتها للصيانة. ستعتمد الواجهة الخلفية على NodeJS و Express مع قاعدة بيانات MongoDB، بينما ستُبنى الواجهة الأمامية باستخدام مكتبة React القوية. لنبدأ بتخطيط واجهة برمجة التطبيقات (API).
بناء واجهة برمجة التطبيقات (API) باستخدام NodeJS و Express و MongoDB و TypeScript
الإعداد الأولي للمشروع
إذا كنت جديدًا في هذا المجال، يمكنك البدء بمراجعة دليل عملي لـ TypeScript أو كيفية بناء واجهة برمجة تطبيقات من الصفر باستخدام NodeJS و Express و MongoDB لتحقيق أقصى استفادة من هذا الشرح. بخلاف ذلك، دعنا ننطلق مباشرة.
لإنشاء تطبيق NodeJS جديد، تحتاج إلى تشغيل الأمر التالي في الطرفية:
yarn init
سيطلب منك هذا الأمر الإجابة على بعض الأسئلة ثم يقوم بتهيئة التطبيق. يمكنك تخطي هذه الأسئلة بإضافة العلامة -y إلى الأمر. بعد ذلك، قم بهيكلة المشروع على النحو التالي:
├── dist
├── node_modules
├── src
├── app.ts
├── controllers
| └── todos
| └── index.ts
├── models
| └── todo.ts
├── routes
| └── index.ts
└── types
└── todo.ts
├── nodemon.json
├── package.json
└── tsconfig.json
كما ترى، هيكل الملفات هذا بسيط نسبيًا. سيعمل المجلد dist كمجلد إخراج بمجرد تجميع التعليمات البرمجية إلى JavaScript عادي. لدينا أيضًا ملف app.ts وهو نقطة الدخول للخادم. توجد المتحكمات (controllers)، والأنواع (types)، والمسارات (routes) في مجلداتها الخاصة.
الآن، نحتاج إلى تهيئة ملف tsconfig.json لمساعدة المترجم (compiler) وفقًا لتفضيلاتنا.
ملف tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist/js",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
],
"exclude": [
"src/types/*.ts",
"node_modules",
".vscode"
]
}
هنا لدينا أربع خصائص رئيسية يجب تسليط الضوء عليها:
outDir: يخبر المترجم بوضع التعليمات البرمجية المجمعة في المجلدdist/js.rootDir: يبلغTypeScriptبتجميع كل ملف.tsموجود في المجلدsrc.include: يخبر المترجم بتضمين الملفات الموجودة في الدليلsrcوالدلائل الفرعية.exclude: سيستبعد الملفات أو المجلدات الممررة في المصفوفة أثناء وقت التجميع.
يمكننا الآن تثبيت التبعيات لتمكين TypeScript في المشروع، لأنه افتراضيًا، سيستخدم هذا التطبيق JavaScript. هناك طريقتان لاستخدام TypeScript في تطبيق NodeJS: إما محليًا في المشروع أو عالميًا على جهازنا. سأختار الطريقة الثانية بناءً على التفضيل الشخصي، ولكن يمكنك الالتزام بالطريقة المحلية إذا أردت ذلك. الآن، دعنا ننفذ الأمر التالي في الطرفية لتثبيت TypeScript:
yarn add typescript -g
تسمح العلامة -g بتثبيت TypeScript عالميًا، مما يجعله متاحًا من أي مكان على الكمبيوتر. بعد ذلك، دعنا نضيف بعض التبعيات لاستخدام Express و MongoDB:
yarn add express cors mongoose
نحتاج أيضًا إلى تثبيت أنواعها (types) كتبعيات تطوير لمساعدة مترجم TypeScript على فهم الحزم:
yarn add -D @types/node @types/express @types/mongoose @types/cors
الآن، لن يثير TypeScript أي اعتراضات بعد الآن، حيث سيستخدم هذه الأنواع لتعريف المكتبات التي قمنا بتثبيتها للتو. نحتاج أيضًا إلى إضافة تبعيات أخرى لتتمكن من تجميع كود TypeScript وبدء تشغيل الخادم بشكل متزامن:
yarn add -D concurrently nodemon
مع هذا الإعداد، يمكننا الآن تحديث ملف package.json بالسكربتات اللازمة لبدء تشغيل الخادم.
ملف package.json
"scripts": {
"build": "tsc",
"start": "concurrently \"tsc -w\" \"nodemon dist/js/app.js\""
}
سيساعد concurrently في تجميع كود TypeScript، ومراقبة التغييرات، وبدء تشغيل الخادم في وقت واحد. بعد قولي هذا، يمكننا الآن تشغيل الخادم، ومع ذلك، لم نقم بإنشاء أي شيء ذي معنى حتى الآن في هذا الصدد. لذلك، دعنا نصلح ذلك في القسم التالي.
إنشاء نوع مهمة (Todo Type)
ملف types/todo.ts
import { Document } from "mongoose"
export interface ITodo extends Document {
name: string
description: string
status: boolean
}
هنا، لدينا واجهة Todo (ITodo) التي توسع النوع Document المقدم من mongoose. سنستخدمها لاحقًا للتفاعل مع MongoDB. بعد قولي هذا، يمكننا الآن تحديد كيف يجب أن يبدو نموذج المهمة (Todo model).
إنشاء نموذج مهمة (Todo Model)
ملف models/todo.ts
import { ITodo } from "./../types/todo"
import { model, Schema } from "mongoose"
const todoSchema: Schema = new Schema(
{
name: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
status: {
type: Boolean,
required: true,
},
},
{ timestamps: true }
)
export default model<ITodo>("Todo", todoSchema)
كما ترون هنا، نبدأ باستيراد الواجهة ITodo وبعض الأدوات المساعدة من mongoose. يساعد هذا الأخير في تحديد مخطط المهمة (Todo schema) وتمرير ITodo كنوع إلى model قبل تصديره. بهذا، يمكننا الآن استخدام نموذج المهمة (Todo model) في ملفات أخرى للتفاعل مع قاعدة البيانات.
إنشاء متحكمات واجهة برمجة التطبيقات (API Controllers): جلب، إضافة، تحديث، وحذف المهام
ملف controllers/todos/index.ts
import { Response, Request } from "express"
import { ITodo } from "./../../types/todo"
import Todo from "../../models/todo"
const getTodos = async (req: Request, res: Response): Promise<void> => {
try {
const todos: ITodo[] = await Todo.find()
res.status(200).json({ todos })
} catch (error) {
throw error
}
}
هنا، نحتاج أولاً إلى استيراد بعض الأنواع من express لأنني أرغب في تحديد قيمها بشكل صريح. إذا أردت، يمكنك ترك TypeScript يستنتجها لك. بعد ذلك، نستخدم الدالة getTodos() لجلب البيانات. تستقبل معلمتين req و res وتعيد وعدًا (promise). وبمساعدة نموذج المهمة (Todo model) الذي أنشأناه سابقًا، يمكننا الآن جلب البيانات من MongoDB وإعادة استجابة تحتوي على مصفوفة المهام.
ملف controllers/todos/index.ts (تابع)
const addTodo = async (req: Request, res: Response): Promise<void> => {
try {
const body = req.body as Pick<ITodo, "name" | "description" | "status">
const todo: ITodo = new Todo({
name: body.name,
description: body.description,
status: body.status,
})
const newTodo: ITodo = await todo.save()
const allTodos: ITodo[] = await Todo.find()
res
.status(201)
.json({ message: "Todo added", todo: newTodo, todos: allTodos })
} catch (error) {
throw error
}
}
كما ترون، تستقبل الدالة addTodo() كائن الجسم (body object) الذي يحتوي على البيانات التي أدخلها المستخدم. بعد ذلك، أستخدم تحويل النوع (typecasting) لتجنب الأخطاء المطبعية وتقييد المتغير body ليتطابق مع ITodo، ثم أنشئ مهمة جديدة (Todo) بناءً على النموذج. بهذا الإعداد، يمكننا الآن حفظ المهمة في قاعدة البيانات وإعادة استجابة تحتوي على المهمة التي تم إنشاؤها ومصفوفة المهام المحدثة.
ملف controllers/todos/index.ts (تابع)
const updateTodo = async (req: Request, res: Response): Promise<void> => {
try {
const {
params: { id },
body,
} = req
const updateTodo: ITodo | null = await Todo.findByIdAndUpdate(
{ _id: id },
body
)
const allTodos: ITodo[] = await Todo.find()
res.status(200).json({ message: "Todo updated", todo: updateTodo, todos: allTodos })
} catch (error) {
throw error
}
}
لتحديث مهمة، نحتاج إلى استخراج المعرف (id) والجسم (body) من الكائن req ثم تمريرهما إلى الدالة findByIdAndUpdate(). هذه الأداة المساعدة ستجد المهمة في قاعدة البيانات وتقوم بتحديثها. وبمجرد اكتمال العملية، يمكننا الآن إعادة البيانات المحدثة إلى المستخدم.
ملف controllers/todos/index.ts (تابع)
const deleteTodo = async (req: Request, res: Response): Promise<void> => {
try {
const deletedTodo: ITodo | null = await Todo.findByIdAndRemove(
req.params.id
)
const allTodos: ITodo[] = await Todo.find()
res.status(200).json({ message: "Todo deleted", todo: deletedTodo, todos: allTodos })
} catch (error) {
throw error
}
}
export { getTodos, addTodo, updateTodo, deleteTodo }
تسمح لك الدالة deleteTodo() بحذف مهمة من قاعدة البيانات. هنا، نستخرج المعرف (id) من req ونمرره كوسيط إلى findByIdAndRemove() للوصول إلى المهمة المقابلة وحذفها من قاعدة البيانات. بعد ذلك، نقوم بتصدير الدوال لنتمكن من استخدامها في ملفات أخرى. بهذا، يمكننا الآن إنشاء بعض المسارات (routes) لواجهة برمجة التطبيقات واستخدام هذه الطرق لمعالجة الطلبات.
إنشاء مسارات واجهة برمجة التطبيقات (API Routes)
ملف routes/index.ts
import { Router } from "express"
import { getTodos, addTodo, updateTodo, deleteTodo } from "../controllers/todos"
const router: Router = Router()
router.get("/todos", getTodos)
router.post("/add-todo", addTodo)
router.put("/edit-todo/:id", updateTodo)
router.delete("/delete-todo/:id", deleteTodo)
export default router
كما ترون هنا، لدينا أربعة مسارات لجلب المهام وإضافتها وتحديثها وحذفها من قاعدة البيانات. وبما أننا أنشأنا الدوال بالفعل، فإن الشيء الوحيد الذي يتعين علينا فعله هو استيراد الطرق وتمريرها كمعلمات لمعالجة الطلبات. لقد غطينا الكثير حتى الآن، ولكن لا يزال ليس لدينا خادم للبدء. لذلك، دعنا نصلح ذلك في القسم التالي.
إنشاء الخادم
قبل إنشاء الخادم، نحتاج أولاً إلى إضافة بعض متغيرات البيئة التي ستحمل بيانات اعتماد MongoDB في ملف nodemon.json.
ملف nodemon.json
{
"env": {
"MONGO_USER": "your-username",
"MONGO_PASSWORD": "your-password",
"MONGO_DB": "your-db-name"
}
}
يمكنك الحصول على بيانات الاعتماد عن طريق إنشاء مجموعة جديدة (cluster) على MongoDB Atlas.
ملف app.ts
import express, { Express } from "express"
import mongoose from "mongoose"
import cors from "cors"
import todoRoutes from "./routes"
const app: Express = express()
const PORT: string | number = process.env.PORT || 4000
app.use(cors())
app.use(todoRoutes)
const uri: string = `mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@clustertodo.raz9g.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&w=majority`
const options = {
useNewUrlParser: true,
useUnifiedTopology: true
}
mongoose.set("useFindAndModify", false)
mongoose
.connect(uri, options)
.then(
() =>
app.listen(PORT, () =>
console.log(`Server running on http://localhost:${PORT}`)
)
)
.catch(error => {
throw error
})
هنا، نبدأ باستيراد مكتبة express التي تسمح لنا بالوصول إلى الدالة use() التي تساعد في معالجة مسارات المهام (Todos routes). بعد ذلك، نستخدم حزمة mongoose للاتصال بـ MongoDB عن طريق إلحاق بيانات الاعتماد الموجودة في ملف nodemon.json بعنوان URL. بهذا، إذا اتصلنا بنجاح بـ MongoDB، سيبدأ الخادم. وإذا حدث خطأ، فسيتم طرحه. لقد انتهينا الآن من بناء واجهة برمجة التطبيقات باستخدام Node و Express و TypeScript و MongoDB. دعنا الآن نبدأ في بناء تطبيق الواجهة الأمامية باستخدام React و TypeScript.

الواجهة الأمامية (Client-side) باستخدام React و TypeScript
الإعداد الأولي للواجهة الأمامية
لإنشاء تطبيق React جديد، سأستخدم create-react-app، ولكن يمكنك استخدام طرق أخرى إذا أردت. لذا، دعنا نشغل الأمر التالي في الطرفية:
npx create-react-app my-app --template typescript
بعد ذلك، قم بتثبيت مكتبة Axios لتتمكن من جلب البيانات عن بعد:
yarn add axios
بمجرد اكتمال التثبيت، دعنا نهيكل مشروعنا على النحو التالي:
├── node_modules
├── public
├── src
| ├── API.ts
| ├── App.test.tsx
| ├── App.tsx
| ├── components
| | ├── AddTodo.tsx
| | └── TodoItem.tsx
| ├── index.css
| ├── index.tsx
| ├── react-app-env.d.ts
| ├── setupTests.ts
| └── type.d.ts
├── tsconfig.json
├── package.json
└── yarn.lock
هنا، لدينا هيكل ملفات بسيط نسبيًا. الشيء الرئيسي الذي يجب ملاحظته هو أن src/type.d.ts سيحتوي على الأنواع (types). وبما أنني سأستخدمها في جميع الملفات تقريبًا، فقد أضفت الامتداد .d.ts لجعل الأنواع متاحة عالميًا. والآن لم نعد بحاجة إلى استيرادها بعد الآن.
إنشاء نوع مهمة (Todo Type) للواجهة الأمامية
ملف src/type.d.ts
interface ITodo {
_id: string
name: string
description: string
status: boolean
createdAt?: string
updatedAt?: string
}
interface TodoProps {
todo: ITodo
}
type ApiDataType = {
message: string
status: string
todos: ITodo[]
todo?: ITodo
}
هنا، يجب أن تعكس واجهة ITodo شكل البيانات القادمة من واجهة برمجة التطبيقات. وبما أننا لا نستخدم mongoose هنا، نحتاج إلى إضافة خصائص إضافية لتتناسب مع النوع المحدد في واجهة برمجة التطبيقات. بعد ذلك، نستخدم نفس الواجهة لـ TodoProps، وهو تعريف النوع للخصائص (props) التي ستستقبلها المكون المسؤول عن عرض البيانات. لقد قمنا الآن بتعريف أنواعنا، دعنا الآن نبدأ في جلب البيانات من واجهة برمجة التطبيقات.
جلب البيانات من واجهة برمجة التطبيقات (API)
ملف src/API.ts
import axios, { AxiosResponse } from "axios"
const baseUrl: string = "http://localhost:4000"
export const getTodos = async (): Promise<AxiosResponse<ApiDataType>> => {
try {
const todos: AxiosResponse<ApiDataType> = await axios.get(
baseUrl + "/todos"
)
return todos
} catch (error) {
throw new Error(error)
}
}
كما ترون، نحتاج إلى استيراد axios لطلب البيانات من واجهة برمجة التطبيقات. بعد ذلك، نستخدم الدالة getTodos() لجلب البيانات من الخادم. ستعيد وعدًا (promise) من النوع AxiosResponse الذي يحمل المهام التي تم جلبها والتي يجب أن تتطابق مع النوع ApiDataType.
ملف src/API.ts (تابع)
export const addTodo = async (
formData: ITodo
): Promise<AxiosResponse<ApiDataType>> => {
try {
const todo: Omit<ITodo, "_id"> = {
name: formData.name,
description: formData.description,
status: false,
}
const saveTodo: AxiosResponse<ApiDataType> = await axios.post(
baseUrl + "/add-todo",
todo
)
return saveTodo
} catch (error) {
throw new Error(error)
}
}
تستقبل هذه الدالة البيانات التي أدخلها المستخدم كوسيط وتعيد وعدًا (promise). هنا، نحتاج إلى حذف الخاصية _id لأن MongoDB سينشئها تلقائيًا.
ملف src/API.ts (تابع)
export const updateTodo = async (
todo: ITodo
): Promise<AxiosResponse<ApiDataType>> => {
try {
const todoUpdate: Pick<ITodo, "status"> = {
status: true,
}
const updatedTodo: AxiosResponse<ApiDataType> = await axios.put(
`${baseUrl}/edit-todo/${todo._id}`,
todoUpdate
)
return updatedTodo
} catch (error) {
throw new Error(error)
}
}
لتحديث مهمة، يجب علينا تمرير البيانات المحدثة والمعرف _id للكائن. هنا، نحتاج إلى تغيير حالة المهمة (status)، ولهذا السبب أختار فقط الخاصية التي نحتاجها قبل إرسال الطلب إلى الخادم.
ملف src/API.ts (تابع)
export const deleteTodo = async (
_id: string
): Promise<AxiosResponse<ApiDataType>> => {
try {
const deletedTodo: AxiosResponse<ApiDataType> = await axios.delete(
`${baseUrl}/delete-todo/${_id}`
)
return deletedTodo
} catch (error) {
throw new Error(error)
}
}
هنا، لدينا أيضًا دالة تستقبل الخاصية _id كمعلمة وتعيد وعدًا (promise). بهذا الإعداد، يمكننا الآن الانتقال إلى مجلد components وإضافة بعض التعليمات البرمجية ذات المعنى إلى ملفاته.
إنشاء المكونات (Components)
مكون إضافة مهمة (Add Todo Form)
سنقوم بإنشاء مكون AddTodo.tsx الذي يسمح للمستخدم بإدخال تفاصيل المهمة الجديدة. سيتضمن هذا المكون نموذجًا بسيطًا مع حقول لاسم المهمة ووصفها، وزر لإرسال البيانات. سيستقبل هذا المكون دالة saveTodo() كخاصية (prop) لمعالجة حفظ البيانات في قاعدة البيانات.
import React, { useState } from "react"
type Props = {
saveTodo: (e: React.FormEvent, formData: ITodo | any) => void
}
const AddTodo: React.FC<Props> = ({ saveTodo }) => {
const [formData, setFormData] = useState<ITodo | {}>({
name: "",
description: "",
status: false,
})
const handleFormChange = (e: React.FormEvent<HTMLInputElement>): void => {
setFormData({
...formData,
[e.currentTarget.id]: e.currentTarget.value,
})
}
return (
<form className="AddTodoForm" onSubmit={(e) => saveTodo(e, formData)}>
<div>
<label htmlFor="name">الاسم</label>
<input onChange={handleFormChange} type="text" id="name" />
</div>
<div>
<label htmlFor="description">الوصف</label>
<input onChange={handleFormChange} type="text" id="description" />
</div>
<button disabled={!formData.name || !formData.description ? true : false}>
إضافة مهمة
</button>
</form>
)
}
export default AddTodo
كما ترون، لدينا هنا مكون وظيفي من النوع React.FC (FC تعني functional component). يستقبل كخاصية (prop) الدالة saveTodo() التي تسمح لنا بحفظ البيانات في قاعدة البيانات. بعد ذلك، لدينا حالة formData التي يجب أن تتطابق مع النوع ITodo لإرضاء المترجم. ولهذا السبب نمررها إلى الخطاف useState. نحتاج أيضًا إلى إضافة نوع بديل ({}) لأن الحالة الأولية ستكون كائنًا فارغًا. وبهذا، يمكننا الآن المضي قدمًا وعرض البيانات التي تم جلبها.
عرض عنصر مهمة (Display a Todo Item)
ملف components/TodoItem.tsx
import React from "react"
type Props = TodoProps & {
updateTodo: (todo: ITodo) => void
deleteTodo: (_id: string) => void
}
const TodoItem: React.FC<Props> = (
{ todo, updateTodo, deleteTodo }
) => {
const checkTodo: string = todo.status ? `line-through` : ""
return (
<div className="Card">
<div className="Card--text">
<h1 className={checkTodo}>{todo.name}</h1>
<span className={checkTodo}>{todo.description}</span>
</div>
<div className="Card--button">
<button
onClick={() => updateTodo(todo)}
className={todo.status ? `hide-button` : "Card--button__done"}
>
إنجاز
</button>
<button
onClick={() => deleteTodo(todo._id)}
className="Card--button__delete"
>
حذف
</button>
</div>
</div>
)
}
export default TodoItem
هنا، نحتاج إلى توسيع نوع TodoProps وإلحاق الدالتين updateTodo و deleteTodo لمعالجة الخصائص (props) التي يستقبلها المكون بشكل مناسب. الآن، بمجرد تمرير كائن المهمة (Todo object)، سنتمكن من عرضه وإضافة الدوال اللازمة لتحديث أو حذف مهمة. ممتاز! يمكننا الآن الانتقال إلى ملف App.tsx وإضافة الجزء الأخير من اللغز.
جلب وعرض البيانات في المكون الرئيسي
ملف App.tsx
import React, { useEffect, useState } from 'react'
import TodoItem from './components/TodoItem'
import AddTodo from './components/AddTodo'
import { getTodos, addTodo, updateTodo, deleteTodo } from './API'
const App: React.FC = () => {
const [todos, setTodos] = useState<ITodo[]>([])
useEffect(
() => {
fetchTodos()
},
[]
)
const fetchTodos = (): void => {
getTodos()
.then(
({ data: { todos } }: ITodo[] | any) => setTodos(todos)
)
.catch((err: Error) => console.log(err))
}
هنا، نحتاج أولاً إلى استيراد المكونات والدوال المساعدة الموجودة في API.ts. بعد ذلك، نمرر إلى useState مصفوفة من النوع ITodo ونهيئها بمصفوفة فارغة. تعيد الدالة getTodos() وعدًا (promise)، وبالتالي يمكننا الوصول إلى الدالة then وتحديث الحالة بالبيانات التي تم جلبها أو طرح خطأ إذا حدث أي شيء. بهذا الإعداد، يمكننا الآن استدعاء الدالة fetchTodos() عند تحميل المكون بنجاح.
ملف App.tsx (تابع)
const handleSaveTodo = (e: React.FormEvent, formData: ITodo): void => {
e.preventDefault()
addTodo(formData)
.then(
({ status, data }) => {
if (status !== 201) {
throw new Error("Error! Todo not saved")
}
setTodos(data.todos)
}
)
.catch(err => console.log(err))
}
const handleUpdateTodo = (todo: ITodo): void => {
updateTodo(todo)
.then(
({ status, data }) => {
if (status !== 200) {
throw new Error("Error! Todo not updated")
}
setTodos(data.todos)
}
)
.catch(err => console.log(err))
}
const handleDeleteTodo = (_id: string): void => {
deleteTodo(_id)
.then(
({ status, data }) => {
if (status !== 200) {
throw new Error("Error! Todo not deleted")
}
setTodos(data.todos)
}
)
.catch(err => console.log(err))
}
بمجرد إرسال النموذج، نستخدم addTodo() لإرسال الطلب إلى الخادم، ثم إذا تم حفظ المهمة بنجاح، نقوم بتحديث البيانات، وإلا فسيتم طرح خطأ.
تتشابه دوال تحديث أو حذف مهمة تمامًا. فكلاهما يستقبل معلمة، ويرسل الطلب، ويستقبل استجابة. ثم يتحققان مما إذا كان الطلب ناجحًا ويتعاملان معه وفقًا لذلك.
ملف App.tsx (تابع)
return (
<main className='App'>
<h1>مهامي</h1>
<AddTodo saveTodo={handleSaveTodo} />
{todos.map((todo: ITodo) => (
<TodoItem
key={todo._id}
updateTodo={handleUpdateTodo}
deleteTodo={handleDeleteTodo}
todo={todo}
/>
))}
</main>
)
}
export default App
هنا، نمر عبر مصفوفة todos ثم نمرر البيانات المتوقعة إلى TodoItem. الآن، إذا تصفحت المجلد الذي يحتوي على تطبيق جانب الخادم (ونفذت الأمر التالي في الطرفية):
yarn start
وأيضًا في تطبيق جانب العميل:
yarn start
يجب أن ترى أن تطبيق المهام الخاص بنا يعمل كما هو متوقع.

رائع! بهذه اللمسة النهائية، انتهينا الآن من بناء تطبيق إدارة المهام باستخدام TypeScript و React و NodeJS و Express و MongoDB. يمكنك العثور على التعليمات البرمجية المصدر هنا.
الخلاصة التقنية
لقد قدم هذا الدليل رحلة شاملة في عالم تطوير تطبيقات الويب المتكاملة، حيث دمجنا قوة TypeScript لتعزيز موثوقية التعليمات البرمجية وقابليتها للصيانة عبر كل من الواجهة الخلفية (NodeJS و Express و MongoDB) والواجهة الأمامية (React). يبرز استخدام TypeScript كعامل حاسم في بناء تطبيقات قابلة للتوسع، خصوصًا في المشاريع الكبيرة التي تتطلب تعريفات أنواع صارمة. كما أن التخطيط الدقيق لواجهة برمجة التطبيقات (API) وإنشاء المتحكمات (controllers) والمسارات (routes) بشكل منظم يضمن بنية خلفية قوية وفعالة. على جانب العميل، يوفر React مرونة عالية في بناء واجهات المستخدم التفاعلية، مدعومًا بـ TypeScript لضمان تطابق البيانات وسهولة التصحيح. هذا النهج المتكامل يمثل نموذجًا مثاليًا لتطوير تطبيقات الويب الحديثة التي تجمع بين الأداء العالي والتجربة الممتازة للمطور والمستخدم على حد سواء.