Kodokon kodokon.com

أنماط الخبراء: المكوّنات المركّبة، عكس التحكّم

صمّم واجهات مكوّنات مرنة عبر المكوّنات المركّبة، ومختزل الحالة، والاختيار الصحيح بين render props والخطّافات.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

مكوّن مثل <Tabs items={...} labels={...} icons={...} /> ينتهي دائماً مدفوناً تحت خصائص التهيئة. نمط المكوّنات المركّبة يقلب المقاربة: تتعاون عدّة مكوّنات وتتشارك حالة ضمنية عبر سياق، بينما يحتفظ المستخدم بالتحكّم في تركيب الـ JSX (الترتيب، التداخل، العناصر المتشابكة). إنه نموذج <select> و<option> في HTML، والنموذج الذي تستعمله مكتبات مثل Radix أو Headless UI.

JSX
const TabsContext = createContext(null);

function Tabs({ defaultTab, children }) {
  const [active, setActive] = useState(defaultTab);
  const value = useMemo(
    () => ({ active, setActive }),
    [active]
  );
  return (
    <TabsContext.Provider value={value}>
      {children}
    </TabsContext.Provider>
  );
}
الأب يكشف الحالة المشتركة عبر سياق محفوظ في الذاكرة.

كل مكوّن فرعي يقرأ السياق ولا يحتاج إلى أيّ خصائص توصيل على الإطلاق: يركّب المستخدم بحرّية، ويضمن النمط الاتّساق. لاحظ قيمة السياق المغلّفة بـ useMemo: من دونها، كان كل عرض للأب سيُنشئ كائناً جديداً ويُجبِر كل المستهلِكين على العرض.

JSX
function Tab({ id, children }) {
  const ctx = useContext(TabsContext);
  return (
    <button
      aria-selected={ctx.active === id}
      onClick={() => ctx.setActive(id)}
    >
      {children}
    </button>
  );
}

// <Tabs defaultTab="a">
//   <Tab id="a">Profile</Tab>
//   <Tab id="b">Settings</Tab>
// </Tabs>
لا خصائص توصيل: السياق يقوم بالربط.

النمط الثاني: عكس التحكّم، وأرقى صوره هو مختزل الحالة الذي نشره Kent C. Dodds في Downshift. بدلاً من إضافة خاصية منطقية لكل حاجة جديدة، يكشف الخطّاف انتقالات حالته: يقدّم المستدعي مختزِلاً يتلقّى كل إجراء فيمكنه تمريره أو تعديله أو إلغاؤه. تحتفظ المكتبة بسلوكها الافتراضي؛ أمّا التحكّم الدقيق فيعود إلى المستهلِك.

JSX
function defaultReducer(state, action) {
  if (action.type === 'toggle') {
    return { on: !state.on };
  }
  return state;
}

function useToggle({ reducer = defaultReducer } = {}) {
  const [state, dispatch] = useReducer(reducer, {
    on: false,
  });
  const toggle = () => dispatch({ type: 'toggle' });
  return { on: state.on, toggle };
}
يستطيع المستدعي اعتراض كل انتقال للحالة.

أخيراً، الجدل الأبدي بين render props والخطّافات. انتصرت الخطّافات في مشاركة المنطق: لا تراتبية زائفة في الشجرة، لا هرم من المغلّفات، تركيب طبيعي لعدّة مصادر. لكن render prop يحتفظ بأرض شرعية: حين تتعلّق التغايرية بـ العرض نفسه. قائمة مُوهَمَة (virtualized) تسألك كيف ترسم كل صفّ، مكوّن headless يفوّض كل JSX الخاص به: هناك، تبقى دالة عرض مُمرَّرة كخاصية الأداة الأكثر مباشرةً، لأن الخطّاف لا يستطيع أن يقرّر أين يُدرِج JSX في شجرة المستدعي.

JSX
function MouseTracker({ render }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  const onMove = (e) =>
    setPos({ x: e.clientX, y: e.clientY });
  return (
    <div onMouseMove={onMove}>{render(pos)}</div>
  );
}

// For the logic alone, a hook is enough:
// const pos = useMousePosition();
render prop لتفويض العرض، وخطّاف للمنطق.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. أيّ آلية تتيح للمكوّنات المركّبة مشاركة حالتها من دون خصائص صريحة؟
    • متغيّر وحدة مشترك
    • سياق React يوفّره المكوّن الأب
    • تكرار الحالة في كل ابن
    • مراجع مُمرَّرة عبر cloneElement
  2. ما الذي يعكسه نمط مختزل الحالة بالضبط؟
    • اتّجاه تدفّق البيانات بين الأب والابن
    • التحكّم في انتقالات الحالة، المُسلَّم إلى المستدعي
    • ترتيب تنفيذ الخطّافات
    • مسؤولية عرض الـ fallback
  3. في أيّ حالة يبقى render prop مفضّلاً على الخطّاف؟
    • حين تريد مشاركة المنطق بين المكوّنات
    • حين يجب على المستدعي تقديم الـ JSX ليُدرَج في شجرة المكوّن
    • حين تحتاج إلى حفظ حساب مكلف في الذاكرة
    • حين لا يملك المكوّن أيّ حالة