Solution to Design Challenge 001: Refactoring with Dependency Inversion
Enhancing Flexibility and Maintainability in React Components
In this issue, we’re diving into a practical application of the Dependency Inversion Principle (DIP) in React. If you’ve been following our Design Challenge series, you’ll remember our discussion about the RequestTypeBuilder
component. Today, we’ll explore a refactoring solution from Pablo Alonso and a variation of my own, highlighting how these approaches exemplify the benefits of DIP.
Pablo’s Solution
In my previous issue (the first in this Design Challenge series), I described a RequestTypeBuilder
component and received a refactoring from Pablo Alonso. I think it’s a great solution that perfectly addresses the coupling problem and offers a flexible design for adding more types.
const NewProjectHeader = () => {
const { user } = useCurrentUser();
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>Choose the request ...</p>
</div>
);
};
const CreatingNewRequestTypeHeader = () => {
return (
<div>
<h1>Select a request type</h1>
<p>Select a template ...</p>
</div>
);
};
const headerComponents = {
creating_new_project: NewProjectHeader,
creating_new_request_type: CreatingNewRequestTypeHeader,
};
const RequestTypeBuilder = ({ renderedIn }) => {
const { templates } = useTemplates(); // fetch templates from remote
const getHeader = () => {
const HeaderComponent = headerComponents[renderedIn];
return HeaderComponent ? <HeaderComponent /> : null;
};
return (
<div>
{getHeader()}
<TemplateSelector templates={templates} />
</div>
);
};
Pablo extracted two individual components, NewProjectHeader
and CreatingNewRequestTypeHeader
, and moved the switch statement into a lookup table (headerComponents
).
This approach moves the logic out of the component, making it much easier to add new types without changing the RequestTypeBuilder
code. For example, to add another header, you simply add it to the headerComponents
map:
const headerComponents = {
creating_new_project: NewProjectHeader,
creating_new_request_type: CreatingNewRequestTypeHeader,
some_other_type: AnotherHeader, // <--- new added header
};
My Solution
My solution is similar to Pablo’s, with one key difference: I removed the renderedIn
prop from the RequestTypeBuilder
and introduced a new prop called header
.
const RequestTypeBuilder = ({ header }) => {
const { templates } = useTemplates(); // fetch templates from remote
return (
<div>
{header}
<TemplateSelector templates={templates} />
</div>
);
};
This follows a common React convention that we pass ReactNode
around. The fundamental idea is the same: the caller decides what to render, not the RequestTypeBuilder
itself.
To ensure the code is easy to extend and modify, you can now use the template list function with different headers:
const NewProject = <RequestTypeBuilder header={<NewProjectHeader />} />
Or with another header:
const FancyRequestTypeBuilder = <RequestTypeBuilder header={
<div>
<h1>I'm a fancy header</h1>
<p>Lorem ipsum</p>
</div>
} />
Dependency Inversion Principle
This is an example of the Dependency Inversion Principle (DIP), which states:
High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).
Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
High-level modules contain the business logic or application-specific logic, defining what needs to be done without concerning themselves with how it is done. Low-level modules handle the specific implementation details.
In our case, RequestTypeBuilder
is the high-level module, while the concrete components like CreateNewProject
and CreateRequestType
are the low-level modules.
Previously, we had low-level logic inside a high-level module:
const getHeader = () => {
switch(renderedIn) {
case 'creating_new_project':
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>Choose the request ... </p>
</div>
);
case 'creating_new_request_type':
return (
<div>
<h1>Select a request type</h1>
<p>Select a template ... </p>
</div>
);
default:
return null;
}
};
This made the high-level module aware of the low-level details. By reversing this dependency, the RequestTypeBuilder
no longer knows about the low-level modules, making it more generic and reusable. We essentially decoupled the builder by introducing an abstraction: the concept of a header.
Now, the header
part is passed in, remaining as a slot from the RequestTypeBuilder
's perspective. Since we don’t specify what a header is inside RequestTypeBuilder
, it can be anything.
For any new types of headers, we don’t need to change anything in the RequestTypeBuilder
. It’s a simple, linear change.
Imagine header A:
Or another different header B:
Conclusion
By applying the Dependency Inversion Principle, we can create more flexible and maintainable React components. Whether you’re working on a large-scale application or a small project, understanding and implementing DIP can significantly improve your code's structure and adaptability.
I hope this issue provides you with valuable insights into refactoring and component design. As always, I encourage you to try these patterns in your projects and share your experiences.
Stay pragmatic, keep coding, and see you in the next issue!
Best,
Juntao Qiu