Why I Went Beyond Optimization and Changed the Structure: The Custom Keypad Story
While running my recipe app, I ran into serious performance and UX problems whenever users loaded and edited large amounts of recipe and ingredient data.
At first, I tried all the usual suspectsāthrottling, partial recalculations, render optimizationsābut it quickly became clear: just "tuning around the edges" wouldn't deliver the stable, smooth UX I wanted.
In the end, I made a bigger decision: rebuild the input flow and state handling, which led to introducing a custom keypad. And that single change made all the difference.
š 1. Performance Problems: The Limits of Optimization in Large Input Scenarios
The old automatic-calculation workflow
When a user clicked "Load Recipe," all their saved ingredients would populate at once. Then, whenever they typed a name or changed a number, a proportional scaling calculation kicked in ~3 seconds later.
This worked fine at first, but the more columns appeared, the worse it got.
Frequent state updates & re-renders
React Hook Form's state was updating constantly, triggering re-renders across the form. Input was getting heavier and slower.
Sync hiccups & lag
Updates sometimes failed to show immediately, or inputs became sluggish for several seconds.
The worst offender: "Unit Cost" mode
The "Unit Cost" screen made this problem exponentially worse. This mode lets you toggle between "Unit Price Input" and "Usage Input". Load just 30 rows and, because the two modes keep separate values, you're actually managing 60 rows of data and fields at the same time.
That means:
- 60 rows registering fields, initializing state, and running calculations all at once
- Compound state changes and re-renders
- CPU usage through the roof
- Calculated results not appearing immediately, making the app feel frozen for seconds
At this point, users understandably asked, "Why is it so slow? Is it broken?"
Why pure optimization wasn't enough
This is a structural bottleneck, not just inefficient code. Partial recalcs, throttling, and render trims helped, but code tweaks alone couldn't remove the problem entirely.
Sometimes the problem isn't in your codeāit's in your approach. When optimization hits its limits, it's time to rethink the structure.
Optimization attemptsāand what remained
Here's what we tried:
- Modified onChangeQuantity to recalculate only the changed row
- Added throttling to limit calculation frequency
- Trimmed down React Hook Form re-renders and memoized components
Example: partial row calculation
interface Ingredient {
ingredient?: string;
quantity?: string | number;
unit?: string;
part?: string;
changedQuantity?: string;
}
interface RecipeFormData {
ingredients: Ingredient[];
currentRatio: number;
}
const useOnChangeQuantity = (formMethods: UseFormReturn<RecipeFormData>) => {
const { getValues, setValue } = formMethods;
const onChangeQuantity = useCallback(
(rowIndex: number): void => {
const ingredient = getValues(`ingredients.${rowIndex}`);
if (!ingredient?.quantity) return;
const currentRatio = getValues("currentRatio");
const calculatedValue = (Number(ingredient.quantity) * currentRatio)
.toFixed(1)
.replace(/\.0$/, "");
if (ingredient.changedQuantity === calculatedValue) return;
setValue(`ingredients.${rowIndex}.changedQuantity`, calculatedValue, {
shouldValidate: false,
shouldDirty: true,
});
},
[getValues, setValue]
);
return onChangeQuantity;
};
These made the app less laggy, but the initial blast of calculations for 60 rows, plus the throttling delay, still left the UX feeling unresponsive.
š¤ 2. UX Problems: Pain Points in Large Forms
Apart from raw performance, the user experience itself had issues:
Excessive clicks and keyboard toggling
The on-screen keyboard popped up and down for every cell.
Tedious column adding
To add a new row, you had to scroll all the way down and tap "Add Ingredient."
Broken flow for continuous input
Focus moved awkwardly, making quick entry frustrating.
End result: even a simple proportion change required back-and-forth between fields and buttons. The input flow just wasn't smooth.
š” 3. The Solution: A Custom Keypad for Both Performance and UX
Why I went structural
The real bottleneck was:
- Large, synchronous state sync in React Hook Form
- All calculations firing in one go
Even maxed-out optimizations couldn't give instant feedback and a natural flow. So I decided to separate the calculation trigger from data entry and reimagine the flow entirely.
The best optimization sometimes isn't optimization at all. It's changing how users interact with your app.
Custom keypad: key features
- Tapping an input brings up a keypad docked above the system keyboard
- Enter ingredient names and quantities in one place
- Only calculates when you tap "Done"ācutting unnecessary re-renders and recalcs
- < and > buttons move between columns
- On the last column, > turns into + to instantly add a new one and keep going
Custom Keypad in Action:
Example:
interface KeypadInputValue {
text?: string;
number?: number;
}
interface OnCompleteProps {
keypadInputValues: Record<number, KeypadInputValue>;
setValue: (name: string, value: unknown) => void;
onChangeQuantity: (rowIndex: number) => void;
setShowKeypad: (show: boolean) => void;
}
const onComplete = ({
keypadInputValues,
setValue,
onChangeQuantity,
setShowKeypad,
}: OnCompleteProps): void => {
// Update form fields from keypad values
Object.entries(keypadInputValues).forEach(([rowIndex, { text, number }]) => {
const idx = Number(rowIndex);
if (text !== undefined) {
setValue(`ingredients[${idx}].ingredient`, text);
}
if (number !== undefined) {
setValue(`ingredients[${idx}].quantity`, number);
onChangeQuantity(idx);
}
});
// Close the keypad after completion
// (Could be split out for strict SRP, but here it's part of "complete input")
setShowKeypad(false);
};
š 4. Impact on Performance and UX
- Performance: No more heavy lag when loading large datasets.
- Stability: Fewer stray state changes ā less risk of data mismatch or bugs.
- UX: Way fewer clicks and keyboard pops, smooth continuous entry.
āļø 5. Before & After
Aspect | Partial Calc + Throttling (only optimize code) | Custom Keypad (structural change) |
---|---|---|
Performance | Fewer ops/renders but still spikes | Clear trigger, wasted work removed |
UX Responsiveness | Possible delay in results | Smooth, uninterrupted flow |
Dev Complexity | Needs ongoing fine-tuning | One-time redesign, easier to maintain |
Bug Risk | Lower, but still possible | Minimized with clearer state timing |
Usability | Frequent clicks & keyboard toggles | Continuous, focused input |
šÆ 6. What I Learned
Separate entry from calculation
Removing unnecessary live state changes improves both speed and clarity.
You don't always need to solve it in code
Sometimes, adjusting the UI flow or UX design achieves more with less effort.
Quick feedback loops matter
Especially in startups, direct, structural solutions can beat endless micro-optimizations.
Not all problems are best solved by "more optimization." Sometimes the most elegant solution is changing how you approach the problem entirely.
This project reminded me that not all problems are best solved by "more optimization." For our team, the custom keypad was a single UI change that boosted stability, sped up the experience, and made the app more enjoyable to use.
If you're wrestling with big forms, heavy state, and tricky UX, maybe it's time to think beyond code tweaks and look at the structure itself.