Inversion of Control ?
Introduction
Inversion of Control ?

Introduction
If you have ever written a lot of code, you already know this truth:
Sometimes, we give way too much control to our components.
Not because we’re bad developers. Not because we don’t understand abstraction. But simply because, in an attempt to make things “reusable,” we accidentally create components that seize behavior instead of hosting it.
Most systems break not because the logic is wrong, but because the control is in the wrong place.
Let’s look at a painfully familiar example.
<DataTable
fetchUrl="/api/users"
enableSearch
enableFiltering
enableSorting
enablePagination
enableCSVExport
filters={[...]}
defaultSort="name"
onRowSelect={() => {}}
onRowExpand={() => {}}
pageSize={20}
debounceMs={300}
retryOnFail
rowVariant="striped"
selectableRows
expandableRows
customCellRenderer={fn}
errorRenderer={fn}
emptyRenderer={fn}
loadingRenderer={fn}
// ...plus 20 more options
/>
You’ve seen this happen:
- functions that decide how to fetch
- classes that create their own dependencies
- components that choose API endpoints
- utilities that hard-code logic
- widgets that “think” they own the business rules
Every time a module takes ownership of behavior that should be supplied from outside, it grows tighter, heavier, and harder to reuse.
The above example is not a DataTable problem, it is a control problem. This is where Inversion of control (IoC) comes to shine.
what is inversion of control
Inversion of Control (IoC) is not a pattern. It’s not a library. It’s not a framework trick.
It is a design philosophy:
The provider controls the lifecycle; the consumer controls the behavior.
Instead of components deciding the rules, they host the rules supplied by their parent. lets think of it this way
Without IoC
The DataTable decides:
- how to fetch
- how to sort
- how to filter
- how to transform rows
- how to debounce
- how to paginate
- when to run all these
It owns the entire flow.
Parent ----> DataTable ----> fetch
----> sort
----> filter
----> debounce
----> pagination
----> render logic
----> error logic
----> row transformation
----> everything else ....
The component owns ALL the logic. see example below
// BLOATED DATATABLE (TOO MUCH CONTROL)
export function DataTable(props) {
const {
fetchUrl,
enableSearch,
enableFiltering,
enableSorting,
enablePagination,
filters,
defaultSort,
pageSize,
debounceMs,
customCellRenderer,
errorRenderer,
emptyRenderer,
loadingRenderer,
} = props;
const [rows, setRows] = useState([]);
const [search, setSearch] = useState("");
const [sort, setSort] = useState(defaultSort);
const [page, setPage] = useState(1);
// It fetches by itself
useEffect(() => {
fetch(fetchUrl)
.then(r => r.json())
.then(data => {
let result = data;
// It sorts by itself
if (enableSorting) {
result = sortRows(result, sort);
}
// It filters by itself
if (enableFiltering) {
result = filterRows(result, filters);
}
// It searches by itself
if (enableSearch) {
result = searchRows(result, search);
}
// It paginates by itself
if (enablePagination) {
result = paginate(result, page, pageSize);
}
setRows(result);
});
}, [fetchUrl, search, sort, page, filters]);
return (
<table>
{/* ... */}
</table>
);
}
With IoC
The DataTable decides only:
- when sorting should trigger
- when filtering should trigger
- when pagination should advance
- when rows should be rendered
But the user injects the behaviors:
- fetch logic
- sorting logic
- filtering logic
- transformation logic
- cell rendering logic
The behavior is injected. The lifecycle is controlled.
Parent ---- injects behaviors ----> fetch()
sort()
filter()
render()
DataTable ---- controls ----> lifecycle
when sorting triggers
when filtering triggers
when pagination happens
when rows re-render
Behavior flows downward from the parent. Lifecycle flows upward from the component.
Let’s rewrite the DataTable using IoC principles. We remove all behavior from the table. We keep only lifecycle + rendering.
// IOC TABLE (LEAN, FLEXIBLE, BEAUTIFUL)
type TableProps<T> = {
rows: T[];
sort: SortState;
onSortChange: (s: SortState) => void;
filters: FilterState;
onFilterChange: (f: FilterState) => void;
children: React.ReactNode;
};
export function Table<T>({
rows,
sort,
onSortChange,
filters,
onFilterChange,
children,
}: TableProps<T>) {
return (
<>
{/* Sorting UI owned by table */}
<SortControls value={sort} onChange={onSortChange} />
{/* Filter UI owned by table */}
<FilterControls value={filters} onChange={onFilterChange} />
<table>
{children}
<tbody>
{rows.map((r, i) => (
<tr key={i}>{/* table only renders */}</tr>
))}
</tbody>
</table>
</>
);
}
The table no longer: fetches, filters, sorts, paginates, transforms, searches, debounces.
It simply orchestrates the lifecycle:
- “Sorting changed — parent, do something.”
- “Filtering changed — parent, do something.”
- “Render these rows.”
Everything else lives in the parent.
Parent Now Controls Behavior
function UsersPage() {
const [sort, setSort] = useState({ column: "name", direction: "asc" });
const [filters, setFilters] = useState({ status: "active" });
const rows = useUserQuery({ sort, filters }); // ← consumer decides behavior
return (
<Table
rows={rows}
sort={sort}
onSortChange={setSort}
filters={filters}
onFilterChange={setFilters}
>
<Table.Column title="Name" render={(user) => user.name} />
<Table.Column title="Email" render={(user) => user.email} />
</Table>
);
}
Now the table doesn’t care if rows come from:
- REST
- GraphQL
- tRPC
- localStorage
- IndexedDB
- mock data
- a cache
- server components
- a streaming API
The behavior is injected. The lifecycle is owned.
This is Inversion of Control.
Conclusion
At the end of the day, this was never about building the “perfect” DataTable. It was about control and why giving components too much of it always backfires.
Once I started using Inversion of Control, my components instantly became lighter, easier to reuse, and way less stressful to maintain. The magic is simple:
Let the component handle when, and let you decide how.
That’s the shift.Stop letting components own your logic. Start letting them host it. Your future self and your entire codebase will thank you.
메타데이터
- post_id
- ae48369bb9e2
- slug
- inversion-of-control-ae48369bb9e2
- url
- https://medium.com/@muyiwamighty/inversion-of-control-ae48369bb9e2
- canonical_url
- https://medium.com/@muyiwamighty/inversion-of-control-ae48369bb9e2
- author_url
- https://medium.com/@muyiwamighty
- status
- ok
- fetched_at
- 2026-08-25 12:53:18