When Default HTML Inputs Fail: Building a Medical-Grade Numeric Input in React

Lahiru ShiranLahiru Shiran
4 min read

When developing healthcare applications, precision isn't just a nice-to-have—it's critical. This article explores how I discovered a subtle but dangerous flaw in the standard HTML number input and built a custom React solution that maintains data integrity for medical applications.

The Unexpected Problem

Copy

<input 
  type="number" 
  min="-20"
  max="20"
  step="0.25"
  value={sphereValue} 
  onChange={(e) => setSphereValue(e.target.value)} 
/>

During testing of an optometry system I built, I noticed something alarming: when doctors entered "+1.50" for a prescription, the native number input silently stripped the plus sign, storing just "1.50".

In most applications, this wouldn't matter. But in ophthalmology, the explicit plus sign has clinical significance—it represents a fundamentally different prescription. This silent data transformation could potentially lead to incorrect lenses being prescribed.

Why Standard Components Fall Short

The standard <input type="number"> has several limitations for medical applications:

  1. It removes leading "+" signs automatically

  2. It applies browser-specific formatting that can't be consistently controlled

  3. It offers limited control over the validation process

  4. It can introduce rounding errors with decimal values

For general use, these behaviors are reasonable. For medical data where precision matters, they're unacceptable.

The Solution: Character-Level Control

The solution was to build a custom component that treats numeric values as strings, giving complete control over character validation, formatting, and display.

Copy

const PrecisionNumericInput: React.FC<NumericInputProps> = ({
  value,
  onChange,
  inputLabel,
  errorCheck,
  ...rest
}) => {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // Only allow digits, plus, minus, and decimal point
    const rawValue = e.target.value?.replace(/[^0-9+-.]/g, "") || "";
    if (onChange) onChange(rawValue);
  };

  const getError = () => {
    if (!value) return false;
    return errorCheck ? errorCheck(value) : false;
  };

  return (
    <TextField
      inputProps={{
        step: 0.25,
        inputMode: "numeric",
        ...rest.inputProps,
      }}
      type="text" // Using text type instead of number
      value={value || ""}
      onChange={handleChange}
      error={getError()}
      size="small"
      label={inputLabel}
      sx={{
        "& .MuiInputBase-root": {
          height: 32,
        },
        ...rest.sx,
      }}
      InputLabelProps={{
        shrink: true,
        ...rest.InputLabelProps,
      }}
      placeholder={inputLabel}
    />
  );
};

Key Design Decisions

  1. Using type="text" instead of type="number" This gives complete control over what characters are permitted and retained

  2. Custom character filtering with regex The component explicitly filters input to only allow digits, signs, and decimal points

  3. Explicit error handling The component accepts a custom validation function for domain-specific rules

  4. Consistent visual styling Material UI's TextField ensures the component feels like a native input while providing more control

Validation Strategy: Trust Nothing

To ensure the component behaved correctly in all scenarios, I created a comprehensive test suite:

Copy

it("preserves leading plus signs", () => {
  fireEvent.change(input, { target: { value: "+2.25" } });
  expect(onChange).toHaveBeenCalledWith("+2.25");
});

it("handles negative values correctly", () => {
  fireEvent.change(input, { target: { value: "-1.75" } });
  expect(onChange).toHaveBeenCalledWith("-1.75");
});

it("strips non-numeric characters", () => {
  fireEvent.change(input, { target: { value: "abc3.50xyz" } });
  expect(onChange).toHaveBeenCalledWith("3.50");
});

it("allows decimal precision to two places", () => {
  fireEvent.change(input, { target: { value: "2.75" } });
  expect(onChange).toHaveBeenCalledWith("2.75");
});

This validation approach ensured the component would handle real-world clinical data entry scenarios reliably.

Integration with Form Libraries

The component works seamlessly with React Hook Form:

Copy

<Controller
  name="sphere_right_eye"
  control={control}
  rules={{ 
    validate: validateSphereValue 
  }}
  render={({ field, fieldState }) => (
    <PrecisionNumericInput
      {...field}
      inputLabel="SPH"
      errorCheck={(val) => !!fieldState.error}
      helperText={fieldState.error?.message}
    />
  )}
/>

Performance Impact

Beyond the clinical benefits, this solution also improved performance in unexpected ways:

  1. Eliminated unnecessary re-renders triggered by browser number formatting

  2. Removed the need for complex state transformations between form submission and API calls

  3. Simplified validation logic by working with consistent string representations

Broader Applications

This approach isn't limited to ophthalmology. Similar precision concerns exist in:

  • Medication dosing calculations

  • Laboratory result entry

  • Vital sign documentation

  • Financial calculations in healthcare billing

Lessons Learned

This experience reinforced several important principles for medical software development:

  1. Never trust default form controls for clinical data
    When precision matters, build custom components from the ground up

  2. Validate with domain experts
    What seems like a minor UI quirk to developers may have significant clinical implications

  3. Test edge cases thoroughly
    Especially scenarios involving decimal precision, leading zeros, and explicit signs

  4. Document the rationale
    Make sure future developers understand why custom components were necessary

Conclusion

Building high-quality medical software often means challenging standard web development practices. While the HTML specification has evolved significantly, there are still cases where the defaults don't align with clinical requirements.

By taking control at the character level, we can build numeric inputs that capture data precisely as clinicians enter it—preserving both the explicit and implicit meaning of the values. This extra effort pays dividends in data integrity, clinical safety, and maintaining the trust of healthcare professionals who depend on our applications.

0
Subscribe to my newsletter

Read articles from Lahiru Shiran directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Lahiru Shiran
Lahiru Shiran