لغة TypeScript

إدارة الحالة الآمنة من حيث النوع

35 دقيقة الدرس 34 من 40

إدارة الحالة الآمنة من حيث النوع

إدارة الحالة أمر حاسم في التطبيقات الحديثة، لكن الحالة غير المكتوبة يمكن أن تؤدي إلى أخطاء وقت التشغيل، وطفرات غير متوقعة، وكوابيس التصحيح. تحول TypeScript إدارة الحالة من خلال توفير ضمانات وقت الترجمة حول شكل الحالة، وأنواع الإجراءات، وقيم إرجاع المحددات. في هذا الدرس، سنستكشف كيفية بناء إدارة حالة آمنة من حيث النوع مع Redux و Zustand والحلول المخصصة التي تكتشف الأخطاء قبل وصولها إلى الإنتاج.

لماذا نكتب الحالة؟

إدارة الحالة الآمنة من حيث النوع توفر فوائد حاسمة:

  • الأمان في وقت الترجمة: اكتشف أخطاء الوصول إلى الحالة والإجراءات غير الصالحة قبل وقت التشغيل
  • الإكمال التلقائي: احصل على IntelliSense لخصائص الحالة والإجراءات والمحددات
  • إعادة الهيكلة: أعد تسمية خصائص الحالة بثقة باستخدام أدوات إعادة الهيكلة في TypeScript
  • التوثيق: الأنواع تعمل كتوثيق حي لشكل الحالة والطفرات
  • القابلية للتنبؤ: فرض انتقالات الحالة الصالحة ومنع الطفرات غير الصالحة

Redux المكتوب مع Redux Toolkit

Redux Toolkit يوفر دعم TypeScript ممتاز جاهز للاستخدام. إليك كيفية الاستفادة منه:

إعداد Redux Store:
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './slices/userSlice';
import postsReducer from './slices/postsSlice';
import uiReducer from './slices/uiSlice';

export const store = configureStore({
  reducer: {
    user: userReducer,
    posts: postsReducer,
    ui: uiReducer,
  },
});

// استنتج أنواع `RootState` و `AppDispatch` من المتجر نفسه
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
خطافات مكتوبة:
// store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './index';

// استخدم في جميع أنحاء تطبيقك بدلاً من `useDispatch` و `useSelector` العادية
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

إنشاء شرائح مكتوبة

عرّف شرائح Redux مكتوبة بقوة مع الحالة والإجراءات والمختزلات:

مثال شريحة المستخدم:
// store/slices/userSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';

// تعريف واجهة الحالة
interface User {
  id: number;
  email: string;
  username: string;
  firstName: string;
  lastName: string;
  avatar?: string;
}

interface UserState {
  currentUser: User | null;
  isAuthenticated: boolean;
  loading: boolean;
  error: string | null;
}

// الحالة الأولية
const initialState: UserState = {
  currentUser: null,
  isAuthenticated: false,
  loading: false,
  error: null,
};

// Async thunk مع قيمة إرجاع مكتوبة
export const fetchUser = createAsyncThunk<
  User, // نوع الإرجاع
  number, // نوع الوسيط
  { rejectValue: string } // تكوين ThunkAPI
>('user/fetchUser', async (userId, { rejectWithValue }) => {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      return rejectWithValue('فشل جلب المستخدم');
    }
    return await response.json();
  } catch (error) {
    return rejectWithValue('خطأ في الشبكة');
  }
});

// إنشاء شريحة مع مختزلات مكتوبة
const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    // إجراء مكتوب مع حمولة
    setUser: (state, action: PayloadAction<User>) => {
      state.currentUser = action.payload;
      state.isAuthenticated = true;
    },
    // إجراء بدون حمولة
    logout: (state) => {
      state.currentUser = null;
      state.isAuthenticated = false;
    },
    // تحديث جزئي مكتوب
    updateUser: (state, action: PayloadAction<Partial<User>>) => {
      if (state.currentUser) {
        state.currentUser = { ...state.currentUser, ...action.payload };
      }
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false;
        state.currentUser = action.payload;
        state.isAuthenticated = true;
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload ?? 'خطأ غير معروف';
      });
  },
});

export const { setUser, logout, updateUser } = userSlice.actions;
export default userSlice.reducer;
ملاحظة: createSlice في Redux Toolkit يولد تلقائيًا منشئي الإجراءات وأنواع الإجراءات من المختزلات الخاصة بك، مع استنتاج TypeScript الكامل لحمولات الإجراءات.

محددات مكتوبة

أنشئ دوال محدد قابلة لإعادة الاستخدام ومكتوبة للوصول إلى الحالة:

أمثلة المحددات:
// store/slices/userSlice.ts (تابع)
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '../index';

// محددات أساسية
export const selectCurrentUser = (state: RootState) => state.user.currentUser;
export const selectIsAuthenticated = (state: RootState) => state.user.isAuthenticated;
export const selectUserLoading = (state: RootState) => state.user.loading;
export const selectUserError = (state: RootState) => state.user.error;

// محدد محفوظ في الذاكرة مع createSelector
export const selectUserFullName = createSelector(
  [selectCurrentUser],
  (user) => user ? `${user.firstName} ${user.lastName}` : null
);

// محدد معقد محفوظ في الذاكرة
export const selectUserDisplayInfo = createSelector(
  [selectCurrentUser, selectIsAuthenticated],
  (user, isAuthenticated) => {
    if (!user || !isAuthenticated) return null;
    return {
      name: `${user.firstName} ${user.lastName}`,
      username: user.username,
      avatar: user.avatar ?? '/default-avatar.png',
    };
  }
);

// الاستخدام في المكونات
import { useAppSelector } from '../../store/hooks';
import { selectUserFullName, selectIsAuthenticated } from '../../store/slices/userSlice';

function UserProfile() {
  const fullName = useAppSelector(selectUserFullName);
  const isAuthenticated = useAppSelector(selectIsAuthenticated);

  if (!isAuthenticated) return <div>الرجاء تسجيل الدخول</div>;
  return <div>مرحبًا، {fullName}</div>;
}

Zustand - حالة خفيفة الوزن مكتوبة

Zustand هي مكتبة إدارة حالة بسيطة مع دعم TypeScript ممتاز:

متجر Zustand الأساسي:
// stores/userStore.ts
import { create } from 'zustand';

interface User {
  id: number;
  email: string;
  username: string;
}

interface UserState {
  currentUser: User | null;
  isAuthenticated: boolean;
  loading: boolean;
  // الإجراءات
  setUser: (user: User) => void;
  logout: () => void;
  updateUser: (updates: Partial<User>) => void;
  fetchUser: (userId: number) => Promise<void>;
}

export const useUserStore = create<UserState>((set, get) => ({
  currentUser: null,
  isAuthenticated: false,
  loading: false,

  setUser: (user) =>
    set({
      currentUser: user,
      isAuthenticated: true,
    }),

  logout: () =>
    set({
      currentUser: null,
      isAuthenticated: false,
    }),

  updateUser: (updates) =>
    set((state) => ({
      currentUser: state.currentUser
        ? { ...state.currentUser, ...updates }
        : null,
    })),

  fetchUser: async (userId) => {
    set({ loading: true });
    try {
      const response = await fetch(`/api/users/${userId}`);
      const user = await response.json();
      set({ currentUser: user, isAuthenticated: true, loading: false });
    } catch (error) {
      set({ loading: false });
      console.error('فشل جلب المستخدم:', error);
    }
  },
}));
استخدام Zustand في المكونات:
// الاستخدام مع أمان نوع كامل
import { useUserStore } from './stores/userStore';

function UserProfile() {
  // اختر شرائح حالة محددة مع استنتاج النوع
  const currentUser = useUserStore((state) => state.currentUser);
  const isAuthenticated = useUserStore((state) => state.isAuthenticated);
  const updateUser = useUserStore((state) => state.updateUser);

  if (!isAuthenticated || !currentUser) {
    return <div>الرجاء تسجيل الدخول</div>;
  }

  return (
    <div>
      <h1>{currentUser.username}</h1>
      <button onClick={() => updateUser({ username: 'اسم_جديد' })}>
        تحديث اسم المستخدم
      </button>
    </div>
  );
}
نصيحة: صيغة محدد Zustand تستنتج تلقائيًا الأنواع من تعريف المتجر الخاص بك. دالة المحدد تتلقى معامل حالة مكتوب بالكامل، مما يوفر إكمال تلقائي ممتاز.

Zustand مع الوسطاء

أضف وسطاء صديقة لـ TypeScript للاستمرارية، وأدوات المطورين، والمزيد:

مثال الوسيط:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { devtools } from 'zustand/middleware';

interface CounterState {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

export const useCounterStore = create<CounterState>()(
  devtools(
    persist(
      (set) => ({
        count: 0,
        increment: () => set((state) => ({ count: state.count + 1 })),
        decrement: () => set((state) => ({ count: state.count - 1 })),
        reset: () => set({ count: 0 }),
      }),
      {
        name: 'counter-storage',
        storage: createJSONStorage(() => localStorage),
      }
    )
  )
);

مدير حالة مخصص آمن من حيث النوع

ابنِ مدير حالة مخصص خفيف الوزن مع دعم TypeScript الكامل:

تنفيذ المتجر المخصص:
type Listener<T> = (state: T) => void;

class TypedStore<T> {
  private state: T;
  private listeners: Set<Listener<T>> = new Set();

  constructor(initialState: T) {
    this.state = initialState;
  }

  getState(): T {
    return this.state;
  }

  setState(updater: Partial<T> | ((state: T) => Partial<T>)): void {
    const updates = typeof updater === 'function' ? updater(this.state) : updater;
    this.state = { ...this.state, ...updates };
    this.notifyListeners();
  }

  subscribe(listener: Listener<T>): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  private notifyListeners(): void {
    this.listeners.forEach((listener) => listener(this.state));
  }
}

// الاستخدام
interface AppState {
  count: number;
  user: { name: string; email: string } | null;
}

const store = new TypedStore<AppState>({
  count: 0,
  user: null,
});

// الاشتراك في التغييرات
const unsubscribe = store.subscribe((state) => {
  console.log('تغيرت الحالة:', state);
});

// تحديث الحالة مع أمان النوع
store.setState({ count: 1 }); // صالح
store.setState((state) => ({ count: state.count + 1 })); // صالح
// store.setState({ invalid: true }); // خطأ TypeScript

أنماط الإجراءات المكتوبة

عرّف أنواع اتحاد مميزة للإجراءات الآمنة من حيث النوع:

أنواع اتحاد الإجراءات:
// تعريف جميع الإجراءات الممكنة
type Action =
  | { type: 'SET_USER'; payload: User }
  | { type: 'LOGOUT' }
  | { type: 'UPDATE_USER'; payload: Partial<User> }
  | { type: 'SET_LOADING'; payload: boolean }
  | { type: 'SET_ERROR'; payload: string };

// منشئو الإجراءات مع كتابة صحيحة
const actions = {
  setUser: (user: User): Action => ({
    type: 'SET_USER',
    payload: user,
  }),
  logout: (): Action => ({
    type: 'LOGOUT',
  }),
  updateUser: (updates: Partial<User>): Action => ({
    type: 'UPDATE_USER',
    payload: updates,
  }),
  setLoading: (loading: boolean): Action => ({
    type: 'SET_LOADING',
    payload: loading,
  }),
  setError: (error: string): Action => ({
    type: 'SET_ERROR',
    payload: error,
  }),
};

// مختزل مع فحص نوع شامل
function reducer(state: UserState, action: Action): UserState {
  switch (action.type) {
    case 'SET_USER':
      return {
        ...state,
        currentUser: action.payload,
        isAuthenticated: true,
      };
    case 'LOGOUT':
      return {
        ...state,
        currentUser: null,
        isAuthenticated: false,
      };
    case 'UPDATE_USER':
      return {
        ...state,
        currentUser: state.currentUser
          ? { ...state.currentUser, ...action.payload }
          : null,
      };
    case 'SET_LOADING':
      return {
        ...state,
        loading: action.payload,
      };
    case 'SET_ERROR':
      return {
        ...state,
        error: action.payload,
      };
    default:
      // فحص الشمولية: خطأ TypeScript إذا كانت أي حالة مفقودة
      const exhaustiveCheck: never = action;
      return state;
  }
}
تحذير: قم دائمًا بتضمين فحص الشمولية default: never في عبارات switch للمختزل الخاص بك. هذا يضمن أن TypeScript سيخطئ إذا أضفت نوع إجراء جديد لكنك نسيت معالجته في المختزل.

الحالة المشتقة مع المحددات

أنشئ قيمًا محسوبة من الحالة مع الحفظ في الذاكرة:

محددات متقدمة:
import { createSelector } from 'reselect'; // أو من @reduxjs/toolkit

// محددات أساسية
const selectPosts = (state: RootState) => state.posts.items;
const selectPostsLoading = (state: RootState) => state.posts.loading;
const selectCurrentUserId = (state: RootState) => state.user.currentUser?.id;

// محددات مشتقة مع الحفظ في الذاكرة
export const selectPublishedPosts = createSelector(
  [selectPosts],
  (posts) => posts.filter((post) => post.published)
);

export const selectCurrentUserPosts = createSelector(
  [selectPosts, selectCurrentUserId],
  (posts, userId) => {
    if (!userId) return [];
    return posts.filter((post) => post.authorId === userId);
  }
);

export const selectPostsByTag = createSelector(
  [selectPosts, (_state: RootState, tag: string) => tag],
  (posts, tag) => posts.filter((post) => post.tags.includes(tag))
);

// محدد مع حساب معقد
export const selectPostsStatistics = createSelector(
  [selectPosts],
  (posts) => ({
    total: posts.length,
    published: posts.filter((p) => p.published).length,
    draft: posts.filter((p) => !p.published).length,
    totalViews: posts.reduce((sum, post) => sum + post.viewCount, 0),
    averageViews: posts.length > 0
      ? posts.reduce((sum, post) => sum + post.viewCount, 0) / posts.length
      : 0,
  })
);
تمرين:
  1. أنشئ شريحة Redux Toolkit لإدارة سلة التسوق مع حالة مكتوبة (العناصر، الكميات، الأسعار)
  2. نفذ إجراءات مكتوبة: addItem، removeItem، updateQuantity، clearCart
  3. أنشئ async thunk لتطبيق رموز الخصم مع كتابة صحيحة
  4. ابنِ محددات محفوظة في الذاكرة لـ: السعر الإجمالي، عدد العناصر، الخصومات المطبقة
  5. أنشئ متجر Zustand لإدارة السمة مع حالة وإجراءات مكتوبة
  6. نفذ وسيطًا للاحتفاظ بالسمة في localStorage مع TypeScript

ملخص

في هذا الدرس، تعلمت كيفية بناء إدارة حالة آمنة من حيث النوع مع Redux Toolkit و Zustand. استكشفت شرائح مكتوبة مع إجراءات ومختزلات، وأنشأت async thunks مكتوبة بقوة، وبنيت محددات محفوظة في الذاكرة للحالة المشتقة، ونفذت مديري حالة مخصصين آمنين من حيث النوع. تعلمت أيضًا كيفية استخدام اتحادات مميزة لمعالجة الإجراءات الشاملة وكيفية الاستفادة من استنتاج نوع TypeScript لكود إدارة حالة أنظف وأكثر قابلية للصيانة. هذه الأنماط تضمن أن إدارة الحالة الخاصة بك قابلة للتنبؤ، وقابلة لإعادة الهيكلة، وخالية من الأخطاء في وقت الترجمة.