Before the weekly menu editor could be built, four supporting features had to exist first: allergens, stations, users, and dishes. Each one is referenced by the menu. None of them could be skipped.
In week five I built all four — and for the first time, the frontend started moving fast.
Why it was suddenly fast#
The first weeks of React were slow. Not because React is hard, but because every decision was new: where does this file go, who owns this state, where should errors be handled?
By week five those questions stopped costing time. The structure and patterns were already established:
features/[name]/
├── pages/ ← fetches data, owns navigation
├── components/ ← receives props, fires callbacks
└── services/ ← one file, one API resourceStarting a new feature meant copying a known template. Services followed the same shape (apiClient calls, same export pattern). Pages started the same way (useState, useEffect, fetch on mount, render). Forms reused the same handleChange and error patterns from earlier weeks.
The question shifted from how do I build this? to what does this feature actually need?
Features that used to take a week now took 2–3 days#
The public menu took days and ended in a refactor. Auth took a full week. Suggestions took most of another week.
This week, allergens, stations, users, and dishes each took roughly 2–3 days — not because they were trivial, but because the friction was gone. Users has role assignment. Station detail has edit and delete. Dishes has a filterable table. The work wasn’t inventing the architecture anymore. It was applying it.
This is the payoff stage: early structure doesn’t speed up the first feature. It speeds up the fifth, sixth, and seventh.
The CSS clicked too#
CSS Modules and shared CSS variables for spacing, colours, and radius made styling faster. Instead of reinventing UI decisions, I applied the same building blocks across features.
CSS Modules kept naming simple and collision-free — .container, .header, .actions work in every file because each module’s class names are scoped at build time. Shared variables meant consistency came for free: var(--color-primary) is the same green on every button without remembering a hex value. The UI stopped feeling like a collection of separate pages and started feeling like one product.
The rabbit hole: allergen icons#
Not everything was fast. Allergen icons were an unplanned detour — and worth every hour.
I found the official EU allergen SVG set on GitHub. Correct content, wrong visual language: thick black borders, background colours that clashed with the app’s design system. So I opened each SVG file and started editing.
An SVG is just XML. The structure is readable once you look at it — fill sets the colour, stroke and stroke-width control the border, viewBox defines the coordinate space. Once that clicked, reworking 14 icons was mechanical: remove the stroke, replace the background fill with a softer tone that matched the design tokens, adjust the viewBox so sizing stayed consistent.
The result was a set of icons that felt native to the app rather than pasted in from somewhere else. AllergenIcon maps each allergen’s displayNumber to its file, with a simple fallback for anything unknown:
const AllergenIcon = ({ displayNumber, size = 'sm' }) => {
const src = allergenIconByDisplayNumber[displayNumber];
if (!src) return <div className={`${styles.fallback} ${styles[size]}`}>?</div>;
return <img className={`${styles.icon} ${styles[size]}`} src={src} loading="lazy" />;
};Here are the final icons after the redesign. The symbols are still the official EU allergen indicators, but the colours, sizing, and visual style were adapted to fit the rest of the application’s design system.
Shared data, one fetch#
Allergens and stations rarely change, so repeatedly fetching them during a short admin session felt unnecessary. Since the JWT already expires after 30 minutes, I chose a simpler approach: fetch the data once when AdminLayout mounts and share it through Outlet context for the rest of the session.
It’s a lightweight caching pattern that keeps navigation fast without introducing additional state management. There are still trade-offs to consider as the application grows, but for now the balance between simplicity and performance feels right.
// AdminLayout.jsx — fetches once on mount
const { allergens } = useAllergens();
const { stations } = useStations();
<Outlet context={{ allergens, stations, ... }} />// Any admin page — receives without fetching
const { allergens, stations } = useOutletContext();Reflections#
The weeks spent on structure, patterns, and shared components were not lost time. They were the investment that made the last few features fast.
A feature-based folder structure doesn’t speed up the first feature. It speeds up the fifth. Shared CSS variables don’t matter when you’re styling the first component. They matter when you’re styling the twentieth and it still looks coherent without thinking about it.
Week five was the first time the architecture felt like it was working for me instead of against me.
Next step#
With the supporting features in place, the weekly menu editor could finally be built — the most complex frontend challenge in the app: a nested interactive grid, state ownership across multiple component levels, and a public display that had to look right with real canteen data.
This is part 15 of the MiseOS development log. Follow along as I build a management tool for professional kitchens, one commit at a time.
