ازاي تبني نظام Undo و Redo في تطبيق React زي المحترفين؟
يا هلا بيك يا صديقي المبرمج! أكيد في يوم من الأيام وأنت شغال على مشروع تطوير الويب (Web Development) وتحديداً لما بتقنبن وتعمل واجهات مستخدم، جالك العميل أو مدير المنتج وقال لك الجملة الشهيرة: "يا باشمهندس، عايزين زرار تراجع (Undo) وزرار تقدم (Redo) زي اللي موجودين في برامج الجرافيك كده!". هنا وشك بيصفّر وتبدأ تفكر، هو أنا هقعد احفظ كل حركة بيعملها المستخدم بإيدي ولا إيه؟ الموضوع ممكن يكون كابوس لو هندسة التطبيق (Application Architecture) مش مصممة صح من الأول.
في المقال ده، هناخد رحلة تفصيلية وعميقة (Deep Dive) عشان نفهم إزاي نبني نظام تراجع وتقدم مرن وقابل للتوسعة في رياكت (React) من غير ما نخلي الكود شبه شربة الهضبة ومن غير ما ندمر أداء التطبيق (Performance).
Table of contents [Show]
إيه هو التحدي الحقيقي في إدارة الحالة (State Management) للـ Undo و Redo؟
المشكلة باختصار إن رياكت في حالتها العادية بتعطيك الـ State بتاعة اللحظة الحالية فقط. لما المستخدم يضغط على زرار، القيمة بتتغير والقيمة القديمة بتروح في خبر كان. عشان نعمل (Undo / Redo)، احنا محتاجين نحتفظ بـ تاريخ أو سجل (History) لكل التعديلات اللي حصلت على الـ State.
لو بنيناها بطريقة عشوائية، ممكن تلاقي الرامات بتاعة المتصفح (Browser Memory) اتملت وبقى فيه بطء رهيب (Memory Leak)، أو تلاقي الكود بقى معقد لدرجة إنك مش عارف تعدل خطأ بسيط فيه. عشان كده لازم نعتمد على استراتيجيات ذكية في إدارة الحالة (State Management Strategies).
الاستراتيجية الأولى: مصفوفة التاريخ (History Array Pattern)
أبسط طريقة وأكثرها انتشاراً هي إننا نحفظ الـ State جوه مصفوفة (Array)، ونعمل مؤشر (Pointer أو Index) بيشاور على المكان اللي إحنا واقفين فيه دلوقتي. كل ما المستخدم يعمل حركة جديدة، بنقطع المصفوفة لحد المكان اللي إحنا واقفين فيه ونضيف الحالة الجديدة، والمؤشر يتحرك خطوة قدام.
عشان نعمل ده بشكل نظير ومنظم، نقدر نكتب هوك مخصص (Custom Hook) يسهل علينا القصة دي. تعال نشوف مثال عملي بالكود:
import { useState, useCallback } from 'react';
export function useUndoRedo(initialPresent) {
const [history, setHistory] = useState([initialPresent]);
const [currentIndex, setCurrentIndex] = useState(0);
const present = history[currentIndex];
const canUndo = currentIndex > 0;
const canRedo = currentIndex < history.length - 1;
const set = useCallback((newPresent) => {
setHistory((prevHistory) => {
const updatedHistory = prevHistory.slice(0, currentIndex + 1);
return [...updatedHistory, newPresent];
});
setCurrentIndex((prevIndex) => prevIndex + 1);
}, [currentIndex]);
const undo = useCallback(() => {
if (canUndo) {
setCurrentIndex((prevIndex) => prevIndex - 1);
}
}, [canUndo]);
const redo = useCallback(() => {
if (canRedo) {
setCurrentIndex((prevIndex) => prevIndex + 1);
}
}, [canRedo]);
return { state: present, set, undo, redo, canUndo, canRedo };
}
الهوك البسيط ده بيحل جزء كبير من المشكلة! الـ set بيضيف الحالة الجديدة للـ History والـ undo و redo بيحركوا المؤشر يمين وشمال بس من غير ما يعيدوا اختراع العجلة.
الاستراتيجية الثانية: استخدام مكتبات جاهزة للإدارة المعقدة
لو التطبيق بتاعك كبير ومليان حالات متشعبة، الفكرة اللي فاتت ممكن تكون كويسة بس هتحتاج شغل كتير لو الـ State متوزعة في حتت كتير. هنا بيجي دور مكتبات إدارة الحالة القوية زي Redux Toolkit أو Zustand.
لو شغال بـ Redux، فيه ميكانيزمات وميدلوير (Redux Middleware) جاهزة زي redux-undo بتعمل لك الـ History تلقائياً لمجرد إنك تلفّت الـ Reducer بتاعك بـ Wrapper معين. دي بتوفر عليك ساعات طويلة من الكتابة والتتست (Testing).
نصائح ذهبية لأداء مذهل وتفادي مشاكل الأداء (Performance Optimization)
لما تيجي تطبق الرهان ده في تطبيق حقيقي، خد بالك من النقط دي كويس جداً:
- تجنب حفظ الـ States الكبيرة جداً بالكامل: لو عندك بيانات ضخمة (Large Datasets)، حفظ الحالة كاملة في كل تكة زرار هيعمل استهلاك عالي للذاكرة. فكر في استخدام تقنيات زي (Immutable Data Structures) أو حفظ التغييرات فقط (Patch/Diffing approach).
- تحديد حجم الـ History (History Limit): مش منطقي تخلي المستخدم يقدر يعمل Undo لـ 10000 خطوة ورا بعض! حدد أقصى عدد خطوات مثلاً آخر 50 خطوة، وده هيحافظ على أداء التطبيق سريع وسلس.
- فصل الـ UI عن منطق التاريخ: خلي مكونات الواجهة (UI Components) ملكش دعوة بالهيصة دي، وخليهم يتعاملوا بس مع الهوك أو الـ Store اللي انت عامله عشان الكود يفضل نضيف وسهل الصيانة (Maintainable Code).
خاتمة ونصيحة أخوية
يا صديقي، ميزات زي الـ Undo و Redo بتفرق جداً في تجربة المستخدم (User Experience - UX) وبتنقل التطبيق بتاعك من مجرد مشروع عادي لمستوى التطبيقات الاحترافية الكبيرة. السر كله بيكمن في التخطيط الصح لهندسة البيانات (Data Architecture) قبل ما تبدأ تكتب أول سطر كود. متستعجلش، جرب الهوك اللي كتبناه فوق وعدل عليه حسب احتياج مشروعك، وكل ما تقابل مشكلة، اعتبرها تحدي برمجي جديد يزود خبرتك.
بالتوفيق في رحلتك البرمجية، ودايماً افتكر إن الكود النظيف هو اللي بيعيش!