إدارة الحالة الآمنة من حيث النوع
إدارة الحالة الآمنة من حيث النوع
إدارة الحالة أمر حاسم في التطبيقات الحديثة، لكن الحالة غير المكتوبة يمكن أن تؤدي إلى أخطاء وقت التشغيل، وطفرات غير متوقعة، وكوابيس التصحيح. تحول TypeScript إدارة الحالة من خلال توفير ضمانات وقت الترجمة حول شكل الحالة، وأنواع الإجراءات، وقيم إرجاع المحددات. في هذا الدرس، سنستكشف كيفية بناء إدارة حالة آمنة من حيث النوع مع Redux و Zustand والحلول المخصصة التي تكتشف الأخطاء قبل وصولها إلى الإنتاج.
لماذا نكتب الحالة؟
إدارة الحالة الآمنة من حيث النوع توفر فوائد حاسمة:
- الأمان في وقت الترجمة: اكتشف أخطاء الوصول إلى الحالة والإجراءات غير الصالحة قبل وقت التشغيل
- الإكمال التلقائي: احصل على IntelliSense لخصائص الحالة والإجراءات والمحددات
- إعادة الهيكلة: أعد تسمية خصائص الحالة بثقة باستخدام أدوات إعادة الهيكلة في TypeScript
- التوثيق: الأنواع تعمل كتوثيق حي لشكل الحالة والطفرات
- القابلية للتنبؤ: فرض انتقالات الحالة الصالحة ومنع الطفرات غير الصالحة
Redux المكتوب مع Redux Toolkit
Redux Toolkit يوفر دعم TypeScript ممتاز جاهز للاستخدام. إليك كيفية الاستفادة منه:
// 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 ممتاز:
// 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);
}
},
}));
// الاستخدام مع أمان نوع كامل
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 مع الوسطاء
أضف وسطاء صديقة لـ 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,
})
);
- أنشئ شريحة Redux Toolkit لإدارة سلة التسوق مع حالة مكتوبة (العناصر، الكميات، الأسعار)
- نفذ إجراءات مكتوبة: addItem، removeItem، updateQuantity، clearCart
- أنشئ async thunk لتطبيق رموز الخصم مع كتابة صحيحة
- ابنِ محددات محفوظة في الذاكرة لـ: السعر الإجمالي، عدد العناصر، الخصومات المطبقة
- أنشئ متجر Zustand لإدارة السمة مع حالة وإجراءات مكتوبة
- نفذ وسيطًا للاحتفاظ بالسمة في localStorage مع TypeScript
ملخص
في هذا الدرس، تعلمت كيفية بناء إدارة حالة آمنة من حيث النوع مع Redux Toolkit و Zustand. استكشفت شرائح مكتوبة مع إجراءات ومختزلات، وأنشأت async thunks مكتوبة بقوة، وبنيت محددات محفوظة في الذاكرة للحالة المشتقة، ونفذت مديري حالة مخصصين آمنين من حيث النوع. تعلمت أيضًا كيفية استخدام اتحادات مميزة لمعالجة الإجراءات الشاملة وكيفية الاستفادة من استنتاج نوع TypeScript لكود إدارة حالة أنظف وأكثر قابلية للصيانة. هذه الأنماط تضمن أن إدارة الحالة الخاصة بك قابلة للتنبؤ، وقابلة لإعادة الهيكلة، وخالية من الأخطاء في وقت الترجمة.