DEV Community

Amber
Amber

Posted on

Building a currency input with React and TypeScript

Hi guys, this is my first post here –and first blog ever. Any feedback is welcome!

A while ago I built a component library for React in TypeScript with styled-components. This library included an input element for large amounts of money, for example 500000. For the sake of readability and convenience, formatting the input like 500.000 was a no-brainer.

Easy, right? Just use an <input type="text" />, apply some formatting by adding JavaScript sprinkles or add a library that does it for you. But this causes a problem. The initial value for the input element comes from an API and is a number, but a text field would convert it into a string. Now we could just parse the string value to a number before passing the property to the parent component and do a request to the server. However, with this approach the component wouldn't work out-of-the-box, since you would need extra logic outside the component to make it work, –which would also neglect the principles of a design system. So how do we handle everything inside this currency component?

I didn't include any design whatsoever, but here you'll find an example.

Let’s start with the interface and definition of our CurrencyInput.

interface Props {
  value: number | undefined;
  updateValue: (value: number | undefined) => void;
}

const CurrencyInput: React.FC<Props> = ({
  value: valueFromProp,
  updateValue,
}) => {
    return (
    <input
      type="text"
      value={currentValue}
      onChange={handleChange}
    />
  )
}

Notice that I renamed the value property to valueFromProp, since we need the name value later on. The value of the input element is set to currentValue and the onChange event triggers the handleChange function.

Adding state with React hooks

Let's add state to the component by using the useState hook.

const CurrencyInput: React.FC<Props> = ({
  value: valueFromProp,
  onChange,
}) => {
  const [currentValue, updateCurrentValue] =12 React.useState<string>(
    valueFromProp ? valueFromProp : ""
  );

  return (
    <input
      type="text"
      value={currentValue}
      onChange={handleChange}
    />
  )
}

We have initiated the state with either valueFromProps or an empty string, but we’re not there yet. The value is a number and the state property expects a string. To solve this, we format the number as string and add a thousands separator with a simple regex expression.

const format = (value: number) =>
  value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");

Updating the state and pass on data

Now we're about to implement the handleChange, a function that updates the state and invokes the callback function updateValue from the parent component.

const CurrencyInput: React.FC<Props> = ({
  value: valueFromProp,
  updateValue,
}) => {
  const [currentValue, updateCurrentValue] = React.useState<string>(
    valueFromProp ? format(valueFromProp) : ""
  );

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { target: { value }} = event;
  }

  return (
    <input
      type="text"
      value={currentValue}
      onChange={handleChange}
    />
  )
}

By destructuring the event object we get the input value. First we need to check if it's empty in which case we pass on undefined to our parent component. If the input isn't empty, we can update our state with a formatted string, and send the value to the parent component as a number. To do this we remove the applied formatting and parse it to a number.

const stringToNumber = (value: string): number => parseInt(value.replace(/\./g, ""), 10);

This is what it looks like when we bring it all together.

interface Props {
  value: number | undefined;
  updateValue: (value: number | undefined) => void;
}

const CurrencyInput: React.FC<Props> = ({
  value: valueFromProp,
  updateValue,
}) => {
  const [currentValue, updateCurrentValue] = React.useState<string>(
    valueFromProp ? format(valueFromProp) : ""
  );

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const {
      target: { value }
    } = event;

    if (value === "") {
      updateCurrentValue("");
      return updateValue(undefined);
    }

    const valueAsNumber = stringToNumber(value);

    updateCurrentValue(format(valueAsNumber));
    return updateValue(valueAsNumber);
  };

  return (
    <input
      type="text"
      value={currentValue}
      onChange={handleChange}
    />
  )
}

Now we have a component with a text field that shows a formatted string, but handles it as a number.

Top comments (1)

Collapse
 
jivkojelev91 profile image
JivkoJelev91

This does not work ...