لغة TypeScript

TypeScript مع React

30 دقيقة الدرس 19 من 40

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>
  );
};
>
نصيحة: طريقة كتابة الخصائص الصريحة (الطريقة 2) مفضلة بشكل عام لأنها أكثر وضوحًا ولا تتضمن خاصية children الضمنية التي يضيفها React.FC.

كتابة الخصائص مع Children

عندما يقبل مكونك عناصر فرعية، استخدم React.ReactNode لأقصى قدر من المرونة.

خصائص Children:
<// عنصر فرعي واحد من نوع محدد
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.

كتابة useState:
<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>
>
ملاحظة: حدد دائمًا نوع عنصر HTML في العام (مثل HTMLInputElement) لفحص دقيق للنوع لهدف الحدث.

كتابة useEffect و useRef

خطاف 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)

تتطلب المكونات ذات الترتيب الأعلى كتابة دقيقة للحفاظ على خصائص المكون الملتف.

كتابة HOC:
<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}
/>
>
تمرين: أنشئ مكون React مكتوب بالكامل يقوم بما يلي:
  1. يقبل قائمة عناصر عبر الخصائص
  2. يدير حالة العنصر المحدد مع useState
  3. يستخدم useEffect لجلب البيانات عند تحميل المكون
  4. يعالج أحداث النقر مع الكتابة المناسبة
  5. يوفر خطافًا مخصصًا للوصول إلى العنصر المحدد
ملخص: يحسن TypeScript بشكل كبير تطوير React من خلال اكتشاف الأخطاء مبكرًا، وتوفير IntelliSense ممتاز، وجعل إعادة الهيكلة أكثر أمانًا. أتقن أنماط الكتابة هذه للتطوير الاحترافي لـ React + TypeScript.