We use cookies to enhance your experience on the site
CodeWorlds

Inversion of Control in React -- Flexible component APIs

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.

What is Inversion of Control?

IoC in React means that a component:

  1. Does NOT hardcode its behavior and appearance
  2. Hands over control to the consumer through props, slots, render props, or callback functions
  3. Becomes flexible -- the same component serves many purposes

Slot Pattern -- named slots

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}

Polymorphic Components -- the
as
prop

Polymorphic 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.

Component Injection

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/>

State Reducer Pattern

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});

Props Collection Pattern

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}

When to use IoC?

| 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 |

Levels of control

IoC in React can be applied at different levels:

  1. Props -- least IoC: component receives data and decides how to display it
  2. Slots/Children -- more IoC: consumer decides about content
  3. Render props / Component injection -- a lot of IoC: consumer decides about entire rendering
  4. State reducer -- maximum IoC: consumer modifies even the state logic

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!

Go to CodeWorlds