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.

main img

🐌 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

AspectPartial Calc + Throttling (only optimize code)Custom Keypad (structural change)
PerformanceFewer ops/renders but still spikesClear trigger, wasted work removed
UX ResponsivenessPossible delay in resultsSmooth, uninterrupted flow
Dev ComplexityNeeds ongoing fine-tuningOne-time redesign, easier to maintain
Bug RiskLower, but still possibleMinimized with clearer state timing
UsabilityFrequent clicks & keyboard togglesContinuous, 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.