TypeScript مع React
TypeScript مع React
TypeScript و React هما مزيج قوي يوفر الأمان من حيث النوع لتطبيقات React الخاصة بك. في هذا الدرس، سنستكشف كيفية كتابة مكونات React والخصائص والحالة والخطافات والأحداث والسياق والمراجع بشكل صحيح للحصول على تجربة تطوير React آمنة تمامًا من حيث النوع.
إعداد React مع TypeScript
أسهل طريقة لبدء مشروع React + TypeScript هي استخدام Create React App مع قالب TypeScript.
<# إنشاء تطبيق React جديد مع TypeScript
npx create-react-app my-app --template typescript
# أو مع Vite (بديل أسرع)
npm create vite@latest my-app -- --template react-ts
# هيكل المشروع
my-app/
├── src/
│ ├── App.tsx # TypeScript + JSX
│ ├── index.tsx
│ └── components/
├── tsconfig.json # تكوين TypeScript
└── package.json
>
كتابة مكونات الدوال
يمكن كتابة مكونات الدوال في TypeScript باستخدام نوع React.FC أو عن طريق كتابة معامل الخصائص بشكل صريح.
<import React from 'react';
// الطريقة 1: استخدام React.FC (مكون وظيفي)
interface UserCardProps {
name: string;
email: string;
age?: number;
}
const UserCard: React.FC<UserCardProps> = ({ name, email, age }) => {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
{age && <p>العمر: {age}</p>}
</div>
);
};
// الطريقة 2: كتابة الخصائص الصريحة (مفضلة)
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
const Button = ({ label, onClick, variant = 'primary', disabled }: ButtonProps) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
};
// مع خاصية children
interface ContainerProps {
title: string;
children: React.ReactNode;
}
const Container = ({ title, children }: ContainerProps) => {
return (
<div>
<h1>{title}</h1>
{children}
</div>
);
};
>
children الضمنية التي يضيفها React.FC.
كتابة الخصائص مع Children
عندما يقبل مكونك عناصر فرعية، استخدم React.ReactNode لأقصى قدر من المرونة.
<// عنصر فرعي واحد من نوع محدد
interface IconProps {
children: React.ReactElement;
size?: number;
}
// عناصر فرعية متعددة
interface CardProps {
children: React.ReactNode; // يمكن أن يكون أي شيء يمكن لـ React عرضه
header?: React.ReactNode;
footer?: React.ReactNode;
}
// نمط render prop
interface DataListProps<T> {
data: T[];
renderItem: (item: T, index: number) => React.ReactNode;
}
function DataList<T>({ data, renderItem }: DataListProps<T>) {
return (
<ul>
{data.map((item, index) => (
<li key={index}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// الاستخدام
<DataList
data={users}
renderItem={(user, index) => (
<div>{user.name}</div>
)}
/>
>
كتابة الحالة مع useState
يمكن لـ TypeScript غالبًا استدلال نوع الحالة من القيمة الأولية، لكن الكتابة الصريحة مفيدة للأنواع المعقدة أو عندما تكون القيمة الأولية null/undefined.
<import { useState } from 'react';
// استدلال النوع (موصى به عندما يكون ممكنًا)
const [count, setCount] = useState(0); // النوع: number
const [name, setName] = useState('جون'); // النوع: string
// كتابة صريحة
const [age, setAge] = useState<number>(25);
// أنواع الاتحاد
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
// كائنات معقدة
interface User {
id: number;
name: string;
email: string;
}
const [user, setUser] = useState<User | null>(null);
// المصفوفات
const [users, setUsers] = useState<User[]>([]);
// تحديث الحالة مع الأمان من حيث النوع
setUser({
id: 1,
name: 'جون',
email: 'john@example.com'
});
// التحديثات الوظيفية آمنة من حيث النوع
setCount(prevCount => prevCount + 1);
setUsers(prevUsers => [...prevUsers, newUser]);
// خطأ: نوع string غير قابل للتعيين لنوع User
// setUser('invalid');
>
كتابة معالجات الأحداث
تحتاج معالجات أحداث React إلى الكتابة بنوع الحدث المناسب للتعامل الآمن مع الأحداث من حيث النوع.
<import { ChangeEvent, FormEvent, MouseEvent, KeyboardEvent } from 'react';
interface FormComponentProps {
onSubmit: (data: { email: string; password: string }) => void;
}
const FormComponent = ({ onSubmit }: FormComponentProps) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// حدث تغيير الإدخال
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};
// حدث إرسال النموذج
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit({ email, password });
};
// حدث نقر الزر
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log('تم النقر على الزر', e.currentTarget);
};
// حدث لوحة المفاتيح
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
console.log('تم الضغط على Enter');
}
};
// معالج حدث مضمن (يعمل استدلال النوع)
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={handleEmailChange}
onKeyDown={handleKeyDown}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" onClick={handleClick}>
إرسال
</button>
</form>
);
};
// أنواع الأحداث الشائعة:
// - MouseEvent<HTMLButtonElement>
// - ChangeEvent<HTMLInputElement>
// - ChangeEvent<HTMLTextAreaElement>
// - ChangeEvent<HTMLSelectElement>
// - FormEvent<HTMLFormElement>
// - KeyboardEvent<HTMLInputElement>
// - FocusEvent<HTMLInputElement>
>
HTMLInputElement) لفحص دقيق للنوع لهدف الحدث.
كتابة useEffect و useRef
خطاف useEffect لا يتطلب كتابة صريحة، لكن useRef يحتاج إلى وسائط النوع للأمان المناسب من حيث النوع.
<import { useEffect, useRef } from 'react';
const Component = () => {
// useEffect - لا حاجة للكتابة، لكن التنظيف يجب أن يطابق نوع الإرجاع
useEffect(() => {
const timer = setTimeout(() => {
console.log('إجراء متأخر');
}, 1000);
// دالة التنظيف
return () => clearTimeout(timer);
}, []);
// مرجع لعناصر DOM
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
// مرجع للقيم القابلة للتغيير
const countRef = useRef<number>(0);
const timerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// وصول مرجع آمن من حيث النوع
if (inputRef.current) {
inputRef.current.focus();
}
if (divRef.current) {
console.log(divRef.current.offsetHeight);
}
}, []);
const startTimer = () => {
timerRef.current = setTimeout(() => {
countRef.current += 1;
}, 1000);
};
const stopTimer = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
};
return (
<div ref={divRef}>
<input ref={inputRef} />
<button onClick={startTimer}>بدء</button>
<button onClick={stopTimer}>إيقاف</button>
</div>
);
};
>
كتابة الخطافات المخصصة
تتبع الخطافات المخصصة نفس أنماط الكتابة كالدوال العادية، مع أنواع الإرجاع الصريحة للوضوح.
<import { useState, useEffect } from 'react';
// خطاف مخصص بسيط
function useToggle(initialValue: boolean = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(prev => !prev);
return [value, toggle];
}
// خطاف مخصص مع خيارات
interface UseFetchOptions {
method?: 'GET' | 'POST';
headers?: Record<string, string>;
}
interface UseFetchReturn<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
function useFetch<T>(url: string, options?: UseFetchOptions): UseFetchReturn<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, options);
const json = await response.json();
setData(json);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
return { data, loading, error, refetch: fetchData };
}
// الاستخدام
interface User {
id: number;
name: string;
}
const UserComponent = () => {
const { data, loading, error } = useFetch<User>('/api/user/123');
if (loading) return <div>جاري التحميل...</div>;
if (error) return <div>خطأ: {error.message}</div>;
if (!data) return null;
return <div>{data.name}</div>;
};
>
كتابة Context API
تتطلب Context API كتابة دقيقة لضمان الأمان من حيث النوع عبر مكونات المزود والمستهلك.
<import { createContext, useContext, useState, ReactNode } from 'react';
// تعريف نوع السياق
interface User {
id: string;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
// إنشاء سياق مع قيمة افتراضية undefined (يضمن استخدام المزود)
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// مكون المزود
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider = ({ children }: AuthProviderProps) => {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
// منطق استدعاء API
const userData = await fetchUser(email, password);
setUser(userData);
};
const logout = () => {
setUser(null);
};
const value: AuthContextType = {
user,
login,
logout,
isAuthenticated: !!user
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
// خطاف مخصص لاستخدام السياق
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('يجب استخدام useAuth داخل AuthProvider');
}
return context;
};
// الاستخدام في المكونات
const ProfileComponent = () => {
const { user, logout, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <div>يرجى تسجيل الدخول</div>;
}
return (
<div>
<h1>{user?.name}</h1>
<button onClick={logout}>تسجيل الخروج</button>
</div>
);
};
>
كتابة خصائص المكونات مع الأنواع العامة
المكونات العامة تسمح بمكونات مرنة وقابلة لإعادة الاستخدام مع الأمان من حيث النوع.
<interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string | number;
}
function Select<T>({
options,
value,
onChange,
getLabel,
getValue
}: SelectProps<T>) {
return (
<select
value={getValue(value)}
onChange={(e) => {
const selectedOption = options.find(
(opt) => getValue(opt).toString() === e.target.value
);
if (selectedOption) {
onChange(selectedOption);
}
}}
>
{options.map((option) => (
<option key={getValue(option)} value={getValue(option)}>
{getLabel(option)}
</option>
))}
</select>
);
}
// الاستخدام مع أنواع مختلفة
interface User {
id: number;
name: string;
}
const UserSelect = () => {
const [selectedUser, setSelectedUser] = useState<User>(users[0]);
return (
<Select
options={users}
value={selectedUser}
onChange={setSelectedUser}
getLabel={(user) => user.name}
getValue={(user) => user.id}
/>
);
};
// يعمل مع الأنواع البدائية أيضًا
const StringSelect = () => {
const [selected, setSelected] = useState('option1');
return (
<Select
options={['option1', 'option2', 'option3']}
value={selected}
onChange={setSelected}
getLabel={(opt) => opt}
getValue={(opt) => opt}
/>
);
};
>
كتابة المكونات ذات الترتيب الأعلى (HOCs)
تتطلب المكونات ذات الترتيب الأعلى كتابة دقيقة للحفاظ على خصائص المكون الملتف.
<import { ComponentType } from 'react';
// الخصائص المحقونة
interface WithLoadingProps {
loading: boolean;
}
// دالة HOC
function withLoading<P extends object>(
Component: ComponentType<P>
): ComponentType<P & WithLoadingProps> {
return (props: P & WithLoadingProps) => {
const { loading, ...rest } = props;
if (loading) {
return <div>جاري التحميل...</div>;
}
return <Component {...(rest as P)} />;
};
}
// الاستخدام
interface UserListProps {
users: User[];
onUserClick: (user: User) => void;
}
const UserList = ({ users, onUserClick }: UserListProps) => {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onUserClick(user)}>
{user.name}
</li>
))}
</ul>
);
};
const UserListWithLoading = withLoading(UserList);
// المكون الآن يتطلب الخصائص الأصلية وخاصية loading
<UserListWithLoading
users={users}
onUserClick={handleClick}
loading={isLoading}
/>
>
- يقبل قائمة عناصر عبر الخصائص
- يدير حالة العنصر المحدد مع useState
- يستخدم useEffect لجلب البيانات عند تحميل المكون
- يعالج أحداث النقر مع الكتابة المناسبة
- يوفر خطافًا مخصصًا للوصول إلى العنصر المحدد