The Starting Problem
In the admin there was this file:
// SavePages.jsx — BEFORE (68 lines)
export const EventSavePage = () => {
const { id } = useParams();
return (
<div className="space-y-6">
<h1 className="text-2xl font-semibold text-gray-900">
{id ? 'Edit Event' : 'Create Event'}
</h1>
<EventForm eventId={id} />
</div>
);
};
export const HeroSlideSavePage = () => {
const { id } = useParams();
return (
<div className="space-y-6">
<h1 className="text-2xl font-semibold text-gray-900">
{id ? 'Edit Hero Slide' : 'Create Hero Slide'}
</h1>
<HeroSlideForm slideId={id} />
</div>
);
};
// ... x6 times, the exact same pattern copiedSix components with identical structure and only three differences between them:
- The label of the title (
'Event','Hero Slide','Partner'…) - The form to render (
EventForm,HeroSlideForm…) - The prop name that receives the id (
eventId,slideId,partnerId…)
The Factory Pattern
What it is
A factory is a function whose only job is to create other things.
normal function: data input → data output
factory: configuration → object / component / function
In JavaScript, when a factory creates React components, it is also known as a Higher-Order Component (HOC): a function that returns a component.
How it works
createSavePage({ label, Form, idProp }) → React Component
The factory receives the configuration that changes and returns a component with the logic that is always the same:
const createSavePage = ({ label, Form, idProp, editOnly = false, extraParams }) => {
const Page = () => {
const params = useParams();
const { id } = params;
const title = editOnly
? `Edit ${label}`
: `${id ? 'Edit' : 'Create'} ${label}`;
const formProps = {
[idProp]: id,
...extraParams?.(params),
};
return (
<div className="space-y-6">
<h1 className="text-2xl font-semibold text-gray-900">{title}</h1>
<Form {...formProps} />
</div>
);
};
Page.displayName = `${label.replace(/\s+/g, '')}SavePage`;
return Page;
};The result
// SavePages.jsx — AFTER (51 lines)
export const EventSavePage = createSavePage({ label: 'Event', Form: EventForm, idProp: 'eventId' });
export const HeroSlideSavePage = createSavePage({ label: 'Hero Slide', Form: HeroSlideForm, idProp: 'slideId' });
export const NewsSavePage = createSavePage({ label: 'News', Form: NewsForm, idProp: 'newsId' });
export const PartnerSavePage = createSavePage({ label: 'Partner', Form: PartnerForm, idProp: 'partnerId' });
export const CommentSavePage = createSavePage({ label: 'Comment', Form: RaceCommentForm, idProp: 'commentId', editOnly: true });
export const EventSectionSavePage = createSavePage({
label: 'Section',
Form: EventSectionForm,
idProp: 'sectionId',
extraParams: (params) => ({ eventId: params.eventId }),
});What it solves: the DRY principle
DRY = Don't Repeat Yourself
Before: changing page design → touching 6 files.
After: changing page design → touching 1 function.
Adding a new page goes from copying and pasting to writing one line:
export const PartnerCategorySavePage = createSavePage({
label: 'Partner Category',
Form: PartnerCategoryForm,
idProp: 'categoryId',
});SOLID Principles Applied
SOLID is a set of 5 principles for writing maintainable code. In this refactoring, two apply primarily.
S — Single Responsibility Principle
Every module should have only one reason to change.
The file SavePages.jsx now has two responsibilities living together:
| Responsibility | Reason to change |
|---|---|
createSavePage (the factory) | If the design or structure of the pages changes |
The exports (EventSavePage, etc.) | If a resource is added or removed |
Should they be separated? Only if both reasons to change occur independently and frequently. See the YAGNI section below.
O — Open/Closed Principle
Code should be open for extension, but closed for modification.
Before, adding a new page meant modifying the existing code (copying one of the components and editing).
Now, adding a new page means extending without touching what already works: a new line calling createSavePage. Existing code remains untouched.
YAGNI — You Aren't Gonna Need It
Always implement things when you actually need them, never when you just foresee that you need them.
YAGNI is an XP (Extreme Programming) principle that combats over-engineering.
What over-engineering is
Adding complexity, abstractions, or structure just in case in the future, when there is no evidence that that future will arrive.
Applied to this case
After the refactoring, the question arises:
"Should I extract
createSavePageto its own fileutils/createSavePage.jsx?"
The SOLID + YAGNI answer:
Is it used in more than one file? NO → leave it where it is
Does it have >30 lines of logic? NO → leave it where it is
Do you want to test it isolated? NO → leave it where it is
Moving it without a real need would add:
- One more file to manage
- An additional import in
SavePages.jsx - More indirection with no tangible benefit
Separate when you have two real reasons to change, not when you might have them.
When to actually extract it
Extracting createSavePage to its own module would make sense if:
- Another file needs to import it
- It grows and contains its own complex logic
- You want to write specific unit tests for the factory
Visual Summary
Problem: Solution: Where it lives:
───────── ───────── ──────────────
EventSavePage ┐ createSavePage() SavePages.jsx
HeroSlideSavePage│ code → ──────────────── (next to the exports,
NewsSavePage │ copied a single definition until there's a reason
PartnerSavePage │ x6 times + 6 lines of config to separate it)
CommentSavePage │
EventSectionPage┘
| Concept | What it says | How it applies here |
|---|---|---|
| Factory | Creates components from config | createSavePage({ label, Form, idProp }) |
| DRY | Don't repeat code | One change affects all 6 components |
| SRP | One responsibility per module | Factory + exports in the same file because they change together |
| OCP | Open to extension, closed to modification | New page = new line, without touching the factory |
| YAGNI | Don't build what you don't need | Not extracting to utils/ without a real need |
| Over-engineering | Complexity without actual benefit | Creating utils/ folder for an 18-line function used in one place |