5 دروس أساسية في React لا تشرحها لك الدروس التقليدية

دقائق القراءة: 8

تعلّم مكتبة React لا يقتصر على فهم الأساسيات مثل المكوّنات، وخصائص props، وإدارة الحالة. فهناك مفاهيم عملية دقيقة لا توضّحها كثير من الدروس، رغم أنها تؤثر مباشرة في جودة التطبيق، وسهولة صيانته، وتجربة المستخدم النهائية. في هذا المقال نستعرض خمس نقاط محورية تساعدك على كتابة تطبيقات React بصورة أكثر نضجاً واحترافية.

خمسة دروس أساسية في React لا تشرحها الشروحات التقليدية للمطورين

1. كيف يتم تحديث الحالة في React فعلياً؟

يعرف معظم المطورين أن الحالة يمكن إنشاؤها وتحديثها باستخدام useState أو useReducer. لكن السؤال الأهم هو: هل يتم تحديث الحالة فوراً بمجرد استدعاء دالة التحديث؟

الإجابة المختصرة: لا، ليس دائماً بشكل فوري. في كثير من الحالات، تقوم React بجدولة التحديث أولاً ثم تنفيذه لاحقاً. لهذا السبب قد لا تحصل على القيمة الجديدة مباشرة داخل نفس السياق التنفيذي.

مثال بسيط على زيادة العداد

import React from 'react' ;

export default function App ( ) {
  const [count, setCount] = React.useState( 0 )

  function addOne ( ) {
    setCount(count + 1 );
  }

  return (
    < div >
      < h1 > Count: {count} </ h1 >
      <button onClick = {addOne} > + 1 </ button >
    </ div >
  );
}

هذا المثال يعمل كما هو متوقع. لكن إذا كررت التحديث مرتين داخل الدالة نفسها، فقد تتوقع أن تزداد القيمة بمقدار 2:

import React from 'react' ;

export default function App ( ) {
  const [count, setCount] = React.useState( 0 )

  function addOne ( ) {
    setCount(count + 1 );
    setCount(count + 1 );
  }

  return (
    < div >
      < h1 > Count: {count} </ h1 >
      <button onClick = {addOne} > + 1 </ button >
    </ div >
  );
}

المفاجأة هنا أن العداد سيزداد بمقدار 1 فقط، وليس 2. السبب أن count لم يتم تحديثه مباشرة بعد الاستدعاء الأول، لذلك يظل يحمل القيمة القديمة نفسها عند الاستدعاء الثاني.

الحل الصحيح: استخدام التحديث المبني على الحالة السابقة

عندما تعتمد القيمة الجديدة على القيمة السابقة، فمن الأفضل استخدام الصيغة الدالية داخل setCount:

import React from 'react' ;

export default function App ( ) {
  const [count, setCount] = React.useState( 0 )

  function addOne ( ) {
    setCount( prevCount => prevCount + 1 );
    setCount( prevCount => prevCount + 1 );
  }

  return (
    < div >
      < h1 > Count: {count} </ h1 >
      <button onClick = {addOne} > + 1 </ button >
    </ div >
  );
}

بهذه الطريقة، تعتمد كل عملية تحديث على أحدث قيمة فعلية، وليس على نسخة قديمة من الحالة.

  • استخدم هذا النمط عندما تكون القيمة الجديدة مشتقة من القيمة السابقة.
  • لا تفترض أن تحديث الحالة متزامن دائماً.
  • فهم آلية الجدولة في React يجنّبك كثيراً من الأخطاء المنطقية.

2. استخدام عدة Effects أفضل من تجميع كل شيء في useEffect واحد

من الأخطاء الشائعة وضع كل الآثار الجانبية داخل useEffect واحد ضخم. صحيح أن ذلك قد يعمل، لكنه يضعف تنظيم الكود ويجعل قراءة المنطق أصعب مع الوقت.

مثال على تجميع أكثر من عملية داخل تأثير واحد

import React from "react" ;

export default function App ( ) {
  const [posts, setPosts] = React.useState([]);
  const [comments, setComments] = React.useState([]);

  React.useEffect( () => {
    fetch( "https://jsonplaceholder.typicode.com/posts" )
      .then( ( res ) => res.json())
      .then( ( data ) => setPosts(data));

    fetch( "https://jsonplaceholder.typicode.com/comments" )
      .then( ( res ) => res.json())
      .then( ( data ) => setComments(data));
  }, []);

  return (
    < div >
      < PostsList posts = {posts} />
      < CommentsList comments = {comments} />
    </ div >
  );
}

هذا الأسلوب ليس مثالياً، لأن جلب المنشورات وجلب التعليقات عمليتان منفصلتان منطقياً. الأفضل فصل كل مهمة داخل useEffect خاص بها.

فصل المسؤوليات يحسّن قابلية الصيانة

import React from "react" ;

export default function App ( ) {
  const [posts, setPosts] = React.useState([]);

  React.useEffect( () => {
    fetch( "https://jsonplaceholder.typicode.com/posts" )
      .then( ( res ) => res.json())
      .then( ( data ) => setPosts(data));
  }, []);

  const [comments, setComments] = React.useState([]);

  React.useEffect( () => {
    fetch( "https://jsonplaceholder.typicode.com/comments" )
      .then( ( res ) => res.json())
      .then( ( data ) => setComments(data));
  }, []);

  return (
    < div >
      < PostsList posts = {posts} />
      < CommentsList comments = {comments} />
    </ div >
  );
}

هذا التقسيم يمنحك فوائد واضحة:

  • تنظيم أفضل للكود.
  • سهولة تتبع وظيفة كل جزء.
  • تبسيط عمليات التعديل والاختبار لاحقاً.
  • تقليل الترابط غير الضروري بين منطق مختلف.

الميزة الكبيرة هنا أن Hooks تسمح لك بترتيب الكود وفقاً للمهمة التي ينفذها، لا وفقاً لدورة حياة المكوّن فقط كما كان يحدث مع المكوّنات الصنفية Class Components.

3. لا تبالغ في تحسين دوال تحديث الحالة باستخدام useCallback

يستخدم كثير من المطورين useCallback لمنع إعادة إنشاء الدوال عند كل إعادة تصيير. وهذا مفيد في بعض الحالات، خصوصاً عند تمرير دوال إلى مكوّنات فرعية حساسة للأداء. لكن الخطأ هو استخدامه بلا حاجة مع دوال تحديث الحالة مثل setState الناتجة عن useState أو useReducer.

مثال على تحسين غير ضروري

import React from "react" ;

export default function App ( ) {
  const [text, setText] = React.useState( "" )

  const handleSetText = React.useCallback( ( event ) => {
    setText(event.target.value);
  }, [])

  return (
    < form >
      < Input text = {text} handleSetText = {handleSetText} />
      <button type = "submit" > Submit </ button >
    </ form >
  );
}

function Input ( { text, handleSetText } ) {
  return (
    <input type = "text" value = {text} onChange = {handleSetText} />
  )
}

رغم أن الكود صحيح، إلا أن تغليف الدالة هنا ليس ضرورياً دائماً. توثيق React يوضح أن هوية دوال التحديث مثل setText مستقرة ولا تتغير بين عمليات إعادة التصيير.

ماذا يعني ذلك عملياً؟

  • لا حاجة إلى تغليف دالة التحديث نفسها داخل useCallback فقط لأنها تستدعي setText.
  • غالباً لا تحتاج إلى إضافة setText إلى مصفوفة الاعتمادات داخل useEffect.
  • التحسين المبالغ فيه قد يزيد التعقيد من دون فائدة حقيقية.

القاعدة المهمة هنا: لا تستخدم أدوات التحسين قبل التأكد من وجود مشكلة أداء فعلية. التحسين غير المدروس قد يجعل الكود أصعب فهماً، وأحياناً يضيف عبئاً بدلاً من تقليله.

4. يمكن لـ useRef الاحتفاظ بالقيم بين عمليات إعادة التصيير

يُنظر إلى useRef غالباً على أنه وسيلة للوصول إلى عنصر في واجهة المستخدم، مثل حقل إدخال أو زر. لكن دوره لا يقتصر على ذلك. إذ يمكن استخدامه أيضاً كحاوية قابلة للتغيير تحتفظ بقيمة ثابتة بين عمليات إعادة التصيير، من دون أن يؤدي تغييرها إلى إعادة التصيير.

لماذا لا نستخدم متغيراً عادياً؟

المتغير العادي يُعاد تهيئته في كل مرة يُعاد فيها تصيير المكوّن. أما القيمة المخزّنة داخل ref.current فتبقى محفوظة عبر جميع عمليات التصيير.

مثال عملي

import React from "react" ;

export default function App ( ) {
  const [count, setCount] = React.useState( 0 );
  const ref = React.useRef({ hasRendered : false });

  React.useEffect( () => {
    if (!ref.current.hasRendered) {
      ref.current.hasRendered = true ;
      console .log( "perform action only once!" );
    }
  }, []);

  return (
    < div >
      <button onClick = {() => setCount(count + 1)}>Count: {count} </ button >
    </ div >
  );
}

في هذا المثال، يتم تنفيذ console.log مرة واحدة فقط. وحتى إذا تغيّرت الحالة عدة مرات، تظل القيمة الموجودة في ref محفوظة.

من أبرز استخدامات useRef:

  • تخزين قيمة لا تحتاج إلى إعادة تصيير عند تغيرها.
  • الاحتفاظ بمعلومة بين عمليات التصيير.
  • تتبّع ما إذا كان حدث معين قد وقع سابقاً.
  • الوصول إلى عناصر DOM مباشرة عند الحاجة.

5. كيف تمنع تطبيق React من الانهيار عند وقوع الأخطاء؟

من أخطر ما قد يحدث في تطبيقات React المنشورة للمستخدمين وقوع خطأ غير معالج يؤدي إلى شاشة فارغة بالكامل. هذا السيناريو شائع إذا لم تتعامل مع الأخطاء بطريقة مدروسة.

مثال على خطأ يتسبب في انهيار الواجهة

import React from "react" ;

export default function App ( ) {
  return (
    <>
      <Header />
    </>
  );
}

function Header ( ) {
  const user = null ;
  return <h1>Hello {user.name} </h1> ;
}

في هذا المثال نحاول الوصول إلى الخاصية name من قيمة null، ما يؤدي إلى خطأ وقت التشغيل. وعند نشر التطبيق في بيئة الإنتاج، قد تظهر للمستخدم شاشة بيضاء فارغة بدلاً من رسالة مفهومة.

تطبيق React يعرض شاشة فارغة بسبب خطأ غير معالج في المكونات

الحل: استخدام Error Boundary

يمكنك حماية أجزاء من شجرة المكوّنات أو التطبيق بالكامل عبر ما يسمى Error Boundary. هذا النمط يلتقط الأخطاء ويعرض واجهة بديلة توضّح للمستخدم أن مشكلة ما حدثت، بدلاً من انهيار التطبيق بصمت.

import React from "react" ;
import { ErrorBoundary } from "react-error-boundary" ;

export default function App ( ) {
  return (
    <ErrorBoundary FallbackComponent = {ErrorFallback} >
      <Header />
    </ ErrorBoundary >
  );
}

function Header ( ) {
  const user = null ;
  return <h1>Hello {user.name} </h1> ;
}

function ErrorFallback ( { error } ) {
  return (
    <div role = "alert" >
      <p>Oops, there was an error:</p>
      <p style = {{ color: "red" }}>{error.message}</p>
    </div>
  );
}

عند وقوع الخطأ، سيظهر للمستخدم محتوى بديل يوضّح وجود مشكلة بدلاً من الشاشة الفارغة.

عرض رسالة خطأ بديلة باستخدام Error Boundary في React بدلاً من انهيار التطبيق

أفضل الممارسات للتعامل مع الأخطاء

  • استخدم Error Boundary حول الأجزاء الحساسة من التطبيق.
  • اعرض رسالة واضحة ومفهومة للمستخدم.
  • أضف خياراً مناسباً مثل إعادة التحميل أو العودة للصفحة الرئيسية.
  • سجّل الأخطاء في خدمة مراقبة إن أمكن، مثل أدوات تتبع الأعطال.

لماذا هذه الدروس مهمة لكل مطور React؟

هذه النقاط الخمس قد تبدو تفصيلية، لكنها في الحقيقة تصنع فرقاً كبيراً بين كتابة كود يعمل فقط، وكتابة كود احترافي سهل الفهم، ومستقر، وقابل للتوسع. المطور المتمرس لا يكتفي بمعرفة طريقة استخدام Hooks، بل يفهم سلوكها الفعلي، ومتى يستخدم كل أداة، ومتى يتجنبها.

  1. فهم تحديث الحالة يجنبك الأخطاء المنطقية الدقيقة.
  2. تقسيم useEffect يجعل الكود أنظف وأسهل صيانة.
  3. تجنّب التحسينات غير الضرورية يقلل التعقيد.
  4. استغلال useRef بذكاء يمنحك مرونة إضافية.
  5. معالجة الأخطاء تحمي تجربة المستخدم وتزيد موثوقية التطبيق.

الخلاصة التقنية

إذا أردت بناء تطبيقات React قوية فعلاً، فلا تكتفِ بما تعرضه الشروحات المبتدئة. الأهم من حفظ الأوامر هو فهم سلوك React تحت الضغط وفي الحالات الواقعية. من وجهة نظر تقنية، أكثر ما يميز المطور المحترف هو قدرته على إدارة الحالة بشكل صحيح، وتنظيم الآثار الجانبية بوضوح، وتفادي التحسينات الوهمية، واستخدام useRef في موضعه المناسب، ثم حماية التطبيق من الانهيار عبر آليات التعامل مع الأخطاء. هذه المبادئ لا تحسن جودة الكود فقط، بل ترفع أيضاً من ثبات المنتج وتجربة المستخدم على المدى الطويل.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *