On a spaceship, every module has a standard socket -- you can connect a plasma engine, an ion drive, or an experimental warp drive to it. The module doesn't decide what engine it should have -- it's the user (engineer) who makes that decision. This is exactly Inversion of Control (IoC) -- the component hands over control to the consumer instead of deciding every detail itself.
IoC in React means that a component:
Instead of a single
children, the component defines multiple slots that the consumer fills with any content:1function SpaceCard({ header, footer, actions, children }) {
2 return (
3 <div className="space-card">
4 {header && <div className="card-header">{header}</div>}
5 <div className="card-body">{children}</div>
6 {actions && <div className="card-actions">{actions}</div>}
7 {footer && <div className="card-footer">{footer}</div>}
8 </div>
9 );
10}
11
12// Consumer decides WHAT to display in each slot
13function MissionCard({ mission }) {
14 return (
15 <SpaceCard
16 header={<h3>{mission.name}</h3>}
17 actions={
18 <>
19 <button>Start</button>
20 <button>Cancel</button>
21 </>
22 }
23 footer={<span>Priority: {mission.priority}</span>}
24 >
25 <p>Distance: {mission.distance} ly</p>
26 <p>Status: {mission.status}</p>
27 </SpaceCard>
28 );
29}as propPolymorphic components allow the consumer to decide which HTML element the component will be:
1function SpaceButton({ as: Component = 'button', variant = 'primary', children, ...props }) {
2 return (
3 <Component className={`space-btn btn-${variant}`} {...props}>
4 {children}
5 </Component>
6 );
7}
8
9// Usage -- same component, different HTML elements
10<SpaceButton onClick={handleClick}>Click</SpaceButton>
11<SpaceButton as="a" href="/missions">Go to missions</SpaceButton>
12<SpaceButton as={Link} to="/dashboard">Dashboard</SpaceButton>
13<SpaceButton as="div" role="button">Div-button</SpaceButton>The
as prop gives full control over the rendered element. The SpaceButton component doesn't need to know whether it will be a <button>, <a>, or <Link> -- the consumer decides.Instead of hardcoding which component to render inside, let the consumer inject any component:
1// List component with injectable item renderer
2function MissionList({ missions, renderItem, emptyState }) {
3 if (missions.length === 0) {
4 return emptyState || <p>No missions</p>;
5 }
6
7 return (
8 <ul>
9 {missions.map((mission, index) => (
10 <li key={mission.id}>
11 {renderItem(mission, index)}
12 </li>
13 ))}
14 </ul>
15 );
16}
17
18// Consumer 1: Simple cards
19<MissionList
20 missions={missions}
21 renderItem={(mission) => <SimpleCard mission={mission} />}
22/>
23
24// Consumer 2: Detailed cards with actions
25<MissionList
26 missions={missions}
27 renderItem={(mission, index) => (
28 <DetailedCard
29 mission={mission}
30 position={index + 1}
31 onDelete={() => removeMission(mission.id)}
32 />
33 )}
34 emptyState={<EmptyMissionsIllustration />}
35/>The most advanced form of IoC -- the consumer can modify the state logic of the component:
1function useToggle({ reducer } = {}) {
2 const [isOpen, setIsOpen] = useState(false);
3
4 const dispatch = (action) => {
5 const nextState = reducer
6 ? reducer(isOpen, action) // Consumer decides!
7 : defaultReducer(isOpen, action); // Default logic
8 setIsOpen(nextState);
9 };
10
11 const toggle = () => dispatch({ type: 'TOGGLE' });
12 const open = () => dispatch({ type: 'OPEN' });
13 const close = () => dispatch({ type: 'CLOSE' });
14
15 return { isOpen, toggle, open, close };
16}
17
18function defaultReducer(state, action) {
19 switch (action.type) {
20 case 'TOGGLE': return !state;
21 case 'OPEN': return true;
22 case 'CLOSE': return false;
23 default: return state;
24 }
25}
26
27// Usage with default logic
28const toggle = useToggle();
29
30// Usage with custom logic -- e.g., blocking close
31const protectedToggle = useToggle({
32 reducer: (state, action) => {
33 if (action.type === 'CLOSE' && !window.confirm('Are you sure?')) {
34 return state; // Don't close!
35 }
36 return defaultReducer(state, action);
37 },
38});The component generates sets of props that the consumer applies to its own elements:
1function useSelection(items) {
2 const [selectedIds, setSelectedIds] = useState(new Set());
3
4 const toggleItem = (id) => {
5 setSelectedIds(prev => {
6 const next = new Set(prev);
7 if (next.has(id)) next.delete(id);
8 else next.add(id);
9 return next;
10 });
11 };
12
13 const getItemProps = (item) => ({
14 onClick: () => toggleItem(item.id),
15 'aria-selected': selectedIds.has(item.id),
16 className: selectedIds.has(item.id) ? 'selected' : '',
17 });
18
19 const getSelectAllProps = () => ({
20 onClick: () => {
21 if (selectedIds.size === items.length) setSelectedIds(new Set());
22 else setSelectedIds(new Set(items.map(i => i.id)));
23 },
24 checked: selectedIds.size === items.length,
25 });
26
27 return {
28 selectedIds,
29 selectedItems: items.filter(i => selectedIds.has(i.id)),
30 getItemProps,
31 getSelectAllProps,
32 };
33}| Situation | Approach | |-----------|----------| | Different appearance, same structure | Slot Pattern | | Different HTML elements | Polymorphic (
as) |
| Different internal components | Component Injection |
| Different state logic | State Reducer |
| Different prop sets | Props Collection |IoC in React can be applied at different levels:
The more IoC, the more flexible the component, but also harder to use. The key is finding the right balance -- like on a spaceship, where too many manual controls slow down the pilot, but too few make it impossible to react to unforeseen situations!