Welcome to the third design challenge of The Pragmatic Developer! In this issue, we tackle a common problem many developers face: dealing with legacy API responses and generating dynamic context menus. This challenge will help you enhance your skills in refactoring and improving code readability while maintaining functionality.
Background and Design Challenge
Recently, I worked on fixing an old bug reported by a customer. The issue, though not affecting many users, highlighted the cumulative maintenance cost of small defects. We decided to address such issues proactively to reduce overall support costs.
The bug involved a context menu that wasn’t functioning correctly — clicking on certain items did nothing. The immediate fix was straightforward: wire up the missing actions to the menu items and thoroughly test the functionality.
During this process, I realized the codebase could benefit from refactoring to improve readability and maintainability. In this design challenge, we’ll explore patterns and techniques to simplify the existing code and make future fixes easier.
Problem Statement
Consider a component that fetches backend data to generate a context menu. Here’s a simplified version of the data fetching, focusing on the already fetched data and ignoring error handling and loading states.
The backend returns data like this:
{
"operations": [
{
"internal_legacy_field": "assign-issue",
"name": "Assign",
"desc": "Assign this issue to someone",
"url": "/issues/assign"
},
{
"internal_legacy_field": "assign-to-me",
"name": "Assign to me",
"desc": "Assign this issue to me",
"url": "/issues/assign-to-me/very/old/outdated/link"
},
{
"internal_legacy_field": "comment-issue",
"name": "Comment",
"desc": "Comment on this issue",
"url": "/comment/issue"
},
{
"internal_legacy_field": "log-work",
"name": "Log work",
"desc": "Log work against this issue"
}
]
}
The API response is in a legacy format and cannot be used directly to create menu items. Some items need to be excluded, some URLs need remapping, and for the remaining items, we’ll generate different types of menu items: ButtonMenuItem
and LinkMenuItem
.
Here’s the current implementation:
const URL_OVERRIDE_FIELDS: Record<string, string> = {
"comment-issue": "/issues/comment",
};
// There are more in the real product
const SKIP_FIELDS: string[] = ["log-work"];
const DIALOG_FIELDS: string[] = ["assign-issue", "comment-issue"];
const IssueContextMenu = ({ data }: { data: IssueResponse }) => {
return (
<DropdownMenu>
{data.operations.reduce(
(result, { name, url: givenUrl, internal_legacy_field }) => {
let url = givenUrl;
if (Object.hasOwn(URL_OVERRIDE_FIELDS, internal_legacy_field)) {
url = URL_OVERRIDE_FIELDS[internal_legacy_field];
}
if (!SKIP_FIELDS.includes(internal_legacy_field)) {
result.push(
DIALOG_FIELDS.includes(internal_legacy_field) ? (
<ButtonMenuItem key={name} name={name} />
) : (
<LinkMenuItem key={name} name={name} url={url ?? ""} />
),
);
}
return result;
},
[] as React.ReactNode[],
)}
</DropdownMenu>
);
};
The current implementation, while functional, is not very readable. I've simplified the code by removing some additional props we pass to ButtonMenuItem
and LinkMenuItem
, which are more than 30 lines long.
Your challenge is to find ways to simplify this code further. You can use any patterns or refactoring techniques you find useful. Think about improving readability and maintainability while ensuring the functionality remains intact.
Feel free to share your solutions and thoughts. In the next issue, I’ll share my approach to this problem.
Thank you for participating in this design challenge! Your input and solutions are invaluable in helping us all grow as developers. I look forward to seeing your innovative approaches. Until next time, happy coding!
Thanks for sharing good exercise!
My first thinking is to use custom hook to extract away these stateful logic, normalise the state needed by the component.
Inside it handles these mapping, exclusion and return a normalised type
type IssueItem {
name: string
url?: string
}
Imagine somewhere inside a certain container component:
const items: []IssueItem = useIssueItems(data: IssueResponse)
Then pass it down to context menu rendering, it's just simple mapping based on the new type
const IssueContextMenu = ({ items }: { items: []IssueItem }) => {
return (
<DropdownMenu>
items.map(item => {item.url ? < LinkMenuItem url={url} /> : < ButtonMenuItem />} )
</DropdownMenu>
)
}
Now component is easier to read, also it decouples it from specific response shape. This could be very helpful if we show menu in different page, when api response changed, we only need to update mapping logic in this single custom hook. All menu should still be able to render.
Keen to check your solution later.