صمّم واجهات مكوّنات مرنة عبر المكوّنات المركّبة، ومختزل الحالة، والاختيار الصحيح بين render props والخطّافات.
افتح هذا الدرس في Kodokonمكوّن مثل <Tabs items={...} labels={...} icons={...} /> ينتهي دائماً مدفوناً تحت خصائص التهيئة. نمط المكوّنات المركّبة يقلب المقاربة: تتعاون عدّة مكوّنات وتتشارك حالة ضمنية عبر سياق، بينما يحتفظ المستخدم بالتحكّم في تركيب الـ JSX (الترتيب، التداخل، العناصر المتشابكة). إنه نموذج <select> و<option> في HTML، والنموذج الذي تستعمله مكتبات مثل Radix أو Headless UI.
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: من دونها، كان كل عرض للأب سيُنشئ كائناً جديداً ويُجبِر كل المستهلِكين على العرض.
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. بدلاً من إضافة خاصية منطقية لكل حاجة جديدة، يكشف الخطّاف انتقالات حالته: يقدّم المستدعي مختزِلاً يتلقّى كل إجراء فيمكنه تمريره أو تعديله أو إلغاؤه. تحتفظ المكتبة بسلوكها الافتراضي؛ أمّا التحكّم الدقيق فيعود إلى المستهلِك.
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 في شجرة المستدعي.
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();