لماذا أشعر بخيبة أمل في الخطافات

تم إعداد ترجمة المقال استعدادًا لبدء الدورة التدريبية "React.js Developer" .










كيف تكون الخطافات مفيدة؟



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



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



الخطافات هي ابتكار في React 16.8 يسمح لك باستخدام ميزات الحالة وميزات React الأخرى دون كتابة فئات.



الرسالة التي أراها هنا تذهب إلى شيء من هذا القبيل: "الفصول الدراسية ليست رائعة!". لا يكفي لتحفيزك على استخدام الخطافات. في رأيي، والسنانير تسمح لنا حل القضايا وظائف الشاملة أكثر أناقة من النهج السابقة: mixins ، مكونات العليا و تقديم الدعائم .



تعد وظائف التسجيل والمصادقة مشتركة بين جميع المكونات ، وتسمح لك الخطافات بإرفاق هذه الوظائف القابلة لإعادة الاستخدام بالمكونات.



ما هو الخطأ في مكونات الفصل؟



هناك بعض الجمال غير المفهوم في مكون عديم الحالة (أي مكون بدون حالة داخلية) يأخذ الخاصيات كمدخلات ويعيد عنصر React. إنها وظيفة خالصة ، أي وظيفة بدون آثار جانبية.



export const Heading: React.FC<HeadingProps> = ({ level, className, tabIndex, children, ...rest }) => {
  const Tag = `h${level}` as Taggable;

  return (
    <Tag className={cs(className)} {...rest} tabIndex={tabIndex}>
      {children}
    </Tag>
  );
};


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



توجد بعض المشكلات المعروفة في أحداث دورة الحياة المستندة إلى الفصل الدراسي. كثير من الناس غير سعداء بأن لديهم لتكرار المنطق في componentDidMountو سائل componentDidUpdate.



async componentDidMount() {
  const response = await get(`/users`);
  this.setState({ users: response.data });
};

async componentDidUpdate(prevProps) {
  if (prevProps.resource !== this.props.resource) {
    const response = await get(`/users`);
    this.setState({ users: response.data });
  }
};


عاجلاً أم آجلاً ، يواجه جميع المطورين هذه المشكلة.



يمكن تنفيذ كود التأثير الجانبي هذا في مكون واحد باستخدام خطاف التأثير.



const UsersContainer: React.FC = () => {
  const [ users, setUsers ] = useState([]);
  const [ showDetails, setShowDetails ] = useState(false);

 const fetchUsers = async () => {
   const response = await get('/users');
   setUsers(response.data);
 };

 useEffect( () => {
    fetchUsers(users)
  }, [ users ]
 );

 // etc.


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



نموذج جافا سكريبت آخر يجب معرفته



عمري 49 عامًا وأنا معجب بـ React. بعد تطوير تطبيق ember مع هذا المراقب وجنون الملكية المحسوب ، سيكون لدي دائمًا شعور دافئ بتدفق البيانات أحادي الاتجاه.



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



مصفوفات التبعية هي الجحيم



و ربط useEffect يمكن أن تتخذ حجة الثانية اختيارية، ودعا مجموعة التبعية، ويسمح لك للاتصال مرة أخرى تأثير عندما كنت في حاجة إليها. لتحديد ما إذا كان التغيير قد حدث أم لا ، تقارن React القيم مع بعضها البعض باستخدام طريقة Object.is . إذا تم تغيير أي عنصر منذ آخر دورة عرض ، فسيتم تطبيق التأثير على القيم الجديدة.



المقارنة رائعة للتعامل مع أنواع البيانات البدائية. ولكن إذا كان أحد العناصر كائنًا أو مصفوفة ، فقد تنشأ مشاكل. يقوم Object.is بمقارنة العناصر والمصفوفات بالإشارة ولا يمكنك فعل أي شيء حيال ذلك. لا يمكن تطبيق خوارزمية المقارنة المخصصة.



التحقق من صحة الكائنات بالإحالة هو حجر عثرة معروف. دعنا نلقي نظرة على نسخة مبسطة من مشكلة واجهتها مؤخرًا.



const useFetch = (config: ApiOptions) => {
  const  [data, setData] = useState(null);

  useEffect(() => {
    const { url, skip, take } = config;
    const resource = `${url}?$skip=${skip}&take=${take}`;
    axios({ url: resource }).then(response => setData(response.data));
  }, [config]); // <-- will fetch on each render

  return data;
};

const App: React.FC = () => {
  const data = useFetch({ url: "/users", take: 10, skip: 0 });
  return <div>{data.map(d => <div>{d})}</div>;
};


في السطر 14 ، useFetchسيتم تمرير كائن جديد إلى الوظيفة لكل تصيير ، ما لم نجعله بحيث يتم استخدام نفس الكائن في كل مرة. في هذا السيناريو ، قد ترغب في التحقق من حقول الكائن ، وليس المرجع إليه.



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



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



يشير وجود المكونات الإضافية مثل use-deep-object-Compar و use-memo-one إلى وجود مشكلة بالفعل (أو على الأقل فوضى).



تعتمد React على ترتيب استدعاء الخطافات



كانت الروابط المخصصة الأولى عبارة عن تطبيقات متعددة لوظيفة معينة useFetchلتقديم الطلبات إلى واجهة برمجة تطبيقات بعيدة. معظمهم لا يحل مشكلة إنشاء طلبات API عن بعد من معالج الأحداث ، لأنه لا يمكن استخدام الخطافات إلا في بداية مكون وظيفي.



ولكن ماذا لو كانت هناك روابط لمواقع مرقمة في البيانات ، ونريد إعادة تشغيل التأثير عندما ينقر المستخدم على الرابط؟ إليك حالة استخدام بسيطة useFetch:



const useFetch = (config: ApiOptions): [User[], boolean] => {
  const [data, setData] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const { skip, take } = config;

    api({ skip, take }).then(response => {
      setData(response);
      setLoading(false);
    });
  }, [config]);

  return [data, loading];
};

const App: React.FC = () => {
  const [currentPage, setCurrentPage] = useState<ApiOptions>({
    take: 10,
    skip: 0
  });

  const [users, loading] = useFetch(currentPage);

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {users.map((u: User) => (
        <div>{u.name}</div>
      ))}
      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li>
            <button onClick={() => console.log(' ?')}>{n + 1}</button>
          </li>
        ))}
      </ul>
    </>
  );
};


في السطر 23 ، useFetchسيتم استدعاء الخطاف مرة واحدة في العرض الأول. في السطور 35-38 ، نقوم بعرض أزرار ترقيم الصفحات. ولكن كيف يمكننا استدعاء الخطاف useFetchمن معالج الأحداث لهذه الأزرار؟



تنص قواعد الخطاف بوضوح على:



لا تستخدم الخطافات داخل الحلقات أو الشرطية أو الوظائف المتداخلة ؛ بدلاً من ذلك ، استخدم الخطافات دائمًا في المستوى الأعلى فقط من وظائف React.



يتم استدعاء الخطافات بنفس الترتيب في كل مرة يتم فيها تقديم المكون. هناك عدة أسباب لذلك ، والتي يمكنك التعرف عليها من هذا المنشور الممتاز.



لا يمكنك القيام بذلك:



<button onClick={() => useFetch({ skip: n + 1 * 10, take: 10 })}>
  {n + 1}
</button>


استدعاء الخطاف useFetchمن معالج الأحداث ينتهك قواعد الخطافات ، لأن ترتيب مكالمتهم يتغير مع كل تصيير.



إرجاع وظيفة قابلة للتنفيذ من الخطاف



أنا على دراية بحلين لهذه المشكلة. يتبعون نفس النهج وأنا أحب كليهما. يُعيد المكون الإضافي لخطاف التفاعل غير المتزامن وظيفة من الخطاف execute:



import { useAsyncCallback } from 'react-async-hook';

const AppButton = ({ onClick, children }) => {
  const asyncOnClick = useAsyncCallback(onClick);
  return (
    <button onClick={asyncOnClick.execute} disabled={asyncOnClick.loading}>
      {asyncOnClick.loading ? '...' : children}
    </button>
  );
};

const CreateTodoButton = () => (
  <AppButton
    onClick={async () => {
      await createTodoAPI('new todo text');
    }}
  >
    Create Todo
  </AppButton>
);


useAsyncCallbackسيؤدي استدعاء الخطاف إلى إرجاع كائن يحمل خصائص التحميل والخطأ والنتيجة المتوقعة ، بالإضافة إلى وظيفة executeيمكن استدعاؤها من معالج الأحداث.



React-hooks-async هو مكون إضافي له نهج مماثل. يستخدم وظيفة useAsyncTask.



إليك مثال كامل بنسخة مبسطة useAsyncTask:



const createTask = (func, forceUpdateRef) => {
  const task = {
    start: async (...args) => {
      task.loading = true;
      task.result = null;
      forceUpdateRef.current(func);
      try {
        task.result = await func(...args);
      } catch (e) {
        task.error = e;
      }
      task.loading = false;
      forceUpdateRef.current(func);
    },
    loading: false,
    result: null,
    error: undefined
  };
  return task;
};

export const useAsyncTask = (func) => {
  const forceUpdate = useForceUpdate();
  const forceUpdateRef = useRef(forceUpdate);
  const task = useMemo(() => createTask(func, forceUpdateRef), [func]);

  useEffect(() => {
    forceUpdateRef.current = f => {
      if (f === func) {
        forceUpdate({});
      }
    };
    const cleanup = () => {
      forceUpdateRef.current = () => null;
    };
    return cleanup;
  }, [func, forceUpdate]);

  return useMemo(
    () => ({
      start: task.start,
      loading: task.loading,
      error: task.error,
      result: task.result
    }),
    [task.start, task.loading, task.error, task.result]
  );
};


تقوم الدالة createTask بإرجاع كائن مهمة بالشكل التالي.



interface Task {
  start: (...args: any[]) => Promise<void>;
  loading: boolean;
  result: null;
  error: undefined;
}


وظيفة والدول ، و التي نتوقع. لكن الدالة ترجع أيضًا دالة startيمكن تسميتها لاحقًا. createTaskلا تؤثر الوظيفة التي تم إنشاؤها باستخدام الوظيفة على التحديث. يتم تشغيل التحديث وظائف forceUpdateو forceUpdateRefفي useAsyncTask.



لدينا الآن وظيفة startيمكن استدعاؤها من معالج الأحداث أو من جزء آخر من التعليمات البرمجية ، وليس بالضرورة من بداية مكون وظيفي.



لكننا فقدنا القدرة على استدعاء الخطاف عند التشغيل الأول للمكون الوظيفي. من الجيد أن يحتوي المكون الإضافي رد فعل-hooks-async على وظيفة useAsyncRun- وهذا يجعل الأمور أسهل:



export const useAsyncRun = (
  asyncTask: ReturnType<typeof useAsyncTask>,
  ...args: any[]
) => {
  const { start } = asyncTask;
  useEffect(() => {
    start(...args);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [asyncTask.start, ...args]);
  useEffect(() => {
    const cleanup = () => {
      //   
    };
    return cleanup;
  });
};


startسيتم تنفيذ الوظيفة كلما تغيرت أي من الوسائط args. الآن يبدو الرمز الذي يحتوي على خطافات كما يلي:



const App: React.FC = () => {
  const asyncTask = useFetch(initialPage);
  useAsyncRun(asyncTask);

  const { start, loading, result: users } = asyncTask;

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {(users || []).map((u: User) => (
        <div>{u.name}</div>
      ))}

      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li key={n}>
            <button onClick={() => start({ skip: 10 * n, take: 10 })}>
              {n + 1}
            </button>
          </li>
        ))}
      </ul>
    </>
  );
};


وفقًا لقواعد الخطافات ، نستخدم خطافًا useFetchفي بداية مكون وظيفي. useAsyncRunتستدعي الدالة API في البداية ، startونستخدم الوظيفة في المعالج onClickلأزرار ترقيم الصفحات.



الآن useFetchيمكن استخدام الخطاف للغرض المقصود منه ، ولكن لسوء الحظ ، عليك أن تسير في الاتجاه الخطأ. نستخدم أيضًا الإغلاق ، والذي يجب أن أعترف أنه يخيفني قليلاً.



التحكم في الخطافات في برامج التطبيق



في برامج التطبيق ، يجب أن يعمل كل شيء على النحو المنشود. إذا كنت تخطط لتتبع المشكلات المتعلقة بالمكونات وتفاعلات المستخدم مع مكونات محددة ، فيمكنك استخدام LogRocket .







LogRocket هو نوع من مسجلات الفيديو لتطبيق الويب الذي يسجل تقريبًا كل ما يحدث على الموقع. يسمح لك المكون الإضافي LogRocket لـ React بالعثور على جلسات المستخدم التي نقر خلالها المستخدم على مكون معين من تطبيقك. ستفهم كيف يتفاعل المستخدمون مع المكونات ولماذا لا تقدم بعض المكونات أي شيء.



يسجل LogRocket جميع الإجراءات والحالات من متجر Redux. إنها مجموعة من الأدوات لتطبيقك تتيح لك تسجيل الطلبات / الردود باستخدام الرؤوس والهيئات. يكتبون HTML و CSS على الصفحة ، ويقدمون عرضًا بكسل تلو الآخر حتى لتطبيقات الصفحة الواحدة الأكثر تعقيدًا.



يقدم LogRocket طريقة حديثة لتصحيح أخطاء تطبيقات React - جربها مجانًا .



خاتمة



أعتقد أن المثال c useFetchيفسر بشكل أفضل سبب شعوري بالإحباط من الخطافات.



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



عمليات الإغلاق (مثل تلك التي تم تمريرها إلى useEffectوuseCallback) الحصول على إصدارات أقدم من الدعائم وقيم الحالة. يحدث هذا ، على سبيل المثال ، عندما يكون أحد المتغيرات الملتقطة مفقودًا في مصفوفة الإدخال لسبب ما - يمكن أن تنشأ صعوبات.



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



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



آمل أن أكون قد أسأت فهم هذا النهج. إذا كان الأمر كذلك ، فاكتب عنها في التعليقات.





اقرأ أكثر:






All Articles