تطبيق عملي: useState و useReducer و Context معاً — أساسيات React Native
تطبيق عملي: useState و useReducer و Context معاً
درست useState في الدرس السابق. التطبيقات الحقيقية تحتاج ثلاث أدوات بحسب حجم الحالة ومن يحتاجها. المثال الكامل يجمعها في شاشة واحدة: عداد، وقائمة مهام، وسمة (فاتح/داكن) مشتركة.
1. useState: حالة بسيطة محلية
function Counter() {
const [count, setCount] = useState(0);
return <Button title={`العداد: ${count}`} onPress={() => setCount((c) => c + 1)} />;
}- استخدم التحديث الوظيفي setCount(c => c + 1) عندما تعتمد القيمة الجديدة على القديمة: التحديثات تُجمع ولا تُطبق فوراً، و setCount(count + 1) مرتين متتاليتين يزيد واحداً فقط.
- لا تعدّل الحالة مباشرة (todos.push(...)): أنشئ نسخة جديدة ([...todos, item]) وإلا لن يعيد React الرسم.
2. useReducer: حالة لها "أفعال" كثيرة
عندما تتغير الحالة بطرق متعددة (إضافة، تبديل، حذف، تعديل، مسح الكل)، تتوزع setState في كل مكان ويصعب تتبعها. useReducer يجمع كل قواعد التغيير في دالة واحدة نقية:
function todoReducer(state, action) {
switch (action.type) {
case "ADD":
return [...state, { id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, text: action.text, done: false }];
case "TOGGLE":
return state.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));
case "DELETE":
return state.filter((t) => t.id !== action.id);
default:
return state;
}
}
// const [todos, dispatch] = useReducer(todoReducer, []);
// dispatch({ type: "ADD", text: "تعلم React Native" });- المكوّن يصف ماذا حدث (dispatch({ type: "TOGGLE", id })) والمخفِّض يقرر كيف تتغير الحالة.
- المخفِّض دالة نقية: لا طلبات شبكة ولا تعديل للحالة القديمة — فيسهل اختباره بلا واجهة.
- المعرّف يجب أن يكون فريداً: Date.now() وحده يتكرر لعنصرين يُضافان في نفس الملّي ثانية.
3. Context: حالة يحتاجها كثيرون
السمة، والمستخدم المسجل، واللغة — تحتاجها شاشات كثيرة متباعدة. تمريرها props عبر خمسة مستويات (prop drilling) مرهق. Context يجعلها متاحة لأي مكوّن تحت المزوّد:
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [dark, setDark] = useState(false);
const value = useMemo(() => ({ dark, toggle: () => setDark((d) => !d) }), [dark]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme يجب أن يُستخدم داخل ThemeProvider");
return ctx;
}- غلّف القيمة بـ useMemo: كائن جديد في كل رسم يجعل كل المستهلكين يعيدون الرسم.
- الخطاف المخصص useTheme يعطي رسالة واضحة إن نُسي المزوّد.
- Context ليس مكاناً لكل الحالة: أي تغيير فيه يعيد رسم كل من يستهلكه. ضع فيه ما يتغير نادراً ويحتاجه الجميع.
أيها أختار؟
| الحالة | الأداة |
|---|---|
| قيمة بسيطة في مكوّن واحد (نص حقل، مفتوح/مغلق) | useState |
| حالة بأفعال متعددة ومنطق انتقال (سلة، نموذج متعدد الخطوات) | useReducer |
| بيانات عامة نادرة التغير (السمة، المستخدم، اللغة) | Context (غالباً مع useReducer أو useState داخل المزوّد) |
| حالة كبيرة مشتركة متغيرة كثيراً | مكتبة: Zustand أو Redux Toolkit (دورة React Native المتقدم) |
| بيانات من الخادم (قوائم، تفاصيل) | مكتبة جلب مع تخزين مؤقت: TanStack Query |
أخطاء شائعة
- وضع كل شيء في Context واحد ضخم فيبطؤ التطبيق كله.
- نسخ props إلى state (useState(props.value)) ثم استغراب عدم تحديثها.
- حالة مشتقة مخزنة: إن أمكن حسابها من حالة أخرى (remaining = todos.filter(...)) فاحسبها أثناء الرسم ولا تخزنها.
تمرين
أضف إلى المثال الكامل فعلين للمخفِّض: EDIT لتعديل نص مهمة و CLEAR_DONE لحذف المنجزة. ثم انقل قائمة المهام إلى Context ليعرض شريط علوي عدد المهام المتبقية دون تمرير props.
// مثال شامل على إدارة الحالة
import React, { useState, useReducer, createContext, useContext } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
// ===== 1. useState - عداد بسيط =====
function Counter() {
const [count, setCount] = useState(0);
return (
<View style={styles.container}>
<Text style={styles.text}>العداد: {count}</Text>
<Button title="زيادة" onPress={() => setCount(c => c + 1)} />
<Button title="تصفير" onPress={() => setCount(0)} />
</View>
);
}
// ===== 2. useReducer - قائمة مهام =====
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD':
return [...state, { id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, text: action.text, done: false }];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
};
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<View>
<Button
title="إضافة مهمة"
onPress={() => dispatch({ type: 'ADD', text: 'مهمة جديدة' })}
/>
{todos.map(todo => (
<View key={todo.id} style={styles.todoItem}>
<Text style={todo.done ? styles.done : null}>{todo.text}</Text>
<Button
title="✓"
onPress={() => dispatch({ type: 'TOGGLE', id: todo.id })}
/>
<Button
title="×"
onPress={() => dispatch({ type: 'DELETE', id: todo.id })}
/>
</View>
))}
</View>
);
}
// ===== 3. Context API - ثيم المظهر =====
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [isDark, setIsDark] = useState(false);
const theme = {
isDark,
colors: isDark
? { background: '#1a1a1a', text: '#fff' }
: { background: '#fff', text: '#000' },
toggleTheme: () => setIsDark(prev => !prev),
};
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
const useTheme = () => useContext(ThemeContext);
function ThemedScreen() {
const { colors, toggleTheme, isDark } = useTheme();
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<Text style={{ color: colors.text }}>
الوضع: {isDark ? 'داكن 🌙' : 'فاتح ☀️'}
</Text>
<Button title="تبديل المظهر" onPress={toggleTheme} />
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 20 },
text: { fontSize: 24, marginBottom: 10 },
todoItem: { flexDirection: 'row', alignItems: 'center', marginVertical: 5 },
done: { textDecorationLine: 'line-through', color: 'gray' },
});
export { Counter, TodoList, ThemeProvider, ThemedScreen };اختبر فهمك