# RIFM for controlled React inputs with stable cursor placement

> RIFM is a small React hook and render-prop component for formatting and masking controlled text inputs while preserving cursor position. It fits React 16.8 and newer, has no runtime dependencies, and provides a string-based number formatter for localized numeric editing.

**TrySound/rifm** — Small react hook and component for building formatted and masked inputs

- Repository: https://github.com/TrySound/rifm
- Website: https://trysound.github.io/rifm/
- Stars: 1,393 · Forks: 37
- Language: TypeScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/trysound-rifm

## Why RIFM targets cursor-aware React input editing

RIFM addresses a specific problem in React forms: applying a format after every keystroke can move the cursor into the wrong position, especially when separators are inserted or removed. Its purpose is to keep the caret in the expected location while the formatted value changes. The README identifies native inputs, textareas, and custom input components as supported targets because RIFM supplies compatible props rather than owning a particular input element.

The library is aimed at developers who control input state in React and need deterministic text transformations. It is not a validation system, a submission library, or a replacement for browser-native numeric and date controls. A caller supplies the current string, an onChange callback that receives the next string, and a format function. RIFM then coordinates formatting, accepted-character tracking, and caret restoration.

The package is described as dependency-free at runtime, ships TypeScript declarations, and supports React 16.8 and newer. Its small size matters for applications that want this behavior without introducing a larger form or masking framework. However, the library's usefulness depends on the formatter being designed around the same accepted characters that RIFM tracks.

## The controlled value and accepted-character mechanism

RIFM is controlled. The value must remain in React state, and both returned properties must be attached to the input. The hook returns an object containing the current value and an onChange handler:

```ts
{
  value: string;
  onChange: React.ChangeEventHandler<E>;
}
```

The onChange callback receives the formatted string rather than a React change event. This makes state integration direct, but it also means the consumer owns persistence and decides how the next value should be stored or handled.

Caret restoration depends on accept, which defaults to a global digit regular expression. RIFM tracks characters matched by that expression before formatting, then uses their order to find a position after separators have moved. The formatter may add, remove, or relocate separators, but it must preserve the order of accepted characters. If a format function removes or reorders digits, the caret calculation no longer has a reliable anchor.

The optional replace function post-processes the formatted value while preserving caret tracking. append handles insertions made at the end, which can help with formatters that add a trailing separator. mask enables replacement-style editing and caret movement across mask separators. These options cover different editing models, but they also create several distinct contracts that the caller must understand before combining them.

## Installing RIFM and creating a formatted price input

The README gives pnpm as the primary installation example and also documents npm and yarn. The package manager metadata identifies pnpm 10.34.5 as the package manager used by the repository.

```sh
pnpm add rifm
```

The first use requires a controlled state value and a formatter. The following example groups digits with commas while preserving the caret around accepted digits:

```tsx
import { useState } from "react";
import { useRifm } from "rifm";

const formatInteger = (value: string) => {
  const digits = value.replace(/\D/g, "");
  return digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};

const [value, setValue] = useState("");
const rifm = useRifm<HTMLInputElement>({
  value,
  onChange: setValue,
  format: formatInteger,
});

<input {...rifm} aria-label="Price" inputMode="numeric" placeholder="0" type="text" />
```

The input should use type="text" with an appropriate inputMode. RIFM explicitly does not support type="number" or type="date" because those controls do not expose the selection APIs needed to restore the caret. That restriction is a hard design boundary, not a style preference.

## Hook and render-prop APIs for native or custom controls

The useRifm hook is convenient when the caller wants input props spread onto an element. The Rifm component offers the same options through a child function, which can be useful when hooks are inconvenient or when the input is rendered through another component.

```tsx
import { useState } from "react";
import { Rifm } from "rifm";

export function PriceInput() {
  const [value, setValue] = useState("");

  return (
    <Rifm value={value} onChange={setValue} format={formatInteger}>
      {({ value, onChange }) => (
        <input type="text" inputMode="numeric" value={value} onChange={onChange} />
      )}
    </Rifm>
  );
}
```

The child receives value and onChange, not a complete set of DOM attributes. A native input, textarea, or custom component can therefore be used when it accepts compatible props. The custom component must preserve the controlled contract and expose any behavior RIFM needs for selection tracking. The README does not document how to adapt a component that hides the underlying input or replaces it with a non-text editing surface, so those integrations require separate verification.

The component form is not a second formatting algorithm. It is an alternative API shape with the same value and transformation contracts.

## The rifm/number formatter and its string-based numeric model

RIFM includes createNumberFormatter under the rifm/number export. With no options, it uses the runtime default locale, groups thousands, accepts positive numbers, and preserves any number of fractional digits. The returned format and accept properties can be spread into useRifm or Rifm.

```tsx
import { createNumberFormatter } from "rifm/number";

const number = createNumberFormatter();
const rifm = useRifm({
  value,
  onChange: setValue,
  ...number,
});
```

This formatter operates on strings rather than JavaScript numbers. The README states that editable states such as 1. and 1.20 are preserved and that large integers do not lose precision. That design is appropriate for an input that remains partially typed. It also means consumers should not assume the formatter is performing numeric conversion or business validation.

Locale-specific behavior comes from Intl.NumberFormat options. The documented options include locales, useGrouping, allowNegative, minimumFractionDigits, and maximumFractionDigits. Fraction digit limits must be non-negative integers, and minimumFractionDigits must not exceed maximumFractionDigits. For example, a German locale formatter can produce a comma as the decimal separator and a period as the grouping separator.

```ts
const euro = createNumberFormatter({
  locales: "de-DE",
  allowNegative: true,
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
});

euro.format("-12345,6");
// "-12.345,60"
```

The format function is locale-aware, but the application still owns whether a value is acceptable for submission.

## Masking constraints, maintenance signals, and upgrade work

The mask option enables replacement-style mask editing and caret movement across mask separators. It is useful for fixed positions, but the README does not provide a full mask syntax specification. The formatter and mask behavior still need to be checked against the exact editing cases required by the application, including deletion, insertion at a separator, and changes in the middle of the value.

The repository is not archived. The last push was on 2026-09-11, and recent releases include v1.1.0 on 2026-08-29 and v1.0.0 on 2026-08-26. The README does not document a compatibility policy, a migration guide for every prior version, or a support timeline. Upgrading therefore requires checking the package exports and API documentation for the target version rather than assuming that the latest version is behaviorally identical to v0.12.0.

The package is MIT licensed, has no runtime dependencies, and publishes only its dist directory according to package.json. The source is TypeScript, the build targets ES2022 and produces ESM output, and peer support is declared for React >=16.8. The MIT license permits use and modification under its terms, but the LICENSE file should be reviewed for the applicable notices.

A general-purpose masking or form library offers a different approach because it may include validation, field composition, and form-state management. RIFM instead focuses on controlled text inputs, accepted-character caret tracking, and a small runtime surface. That narrower design can simplify integration, but missing form features are not supplied by this package.

## Conclusion

RIFM is a good fit for teams building controlled React text inputs for formatted numbers, masks, or transformed text, especially when cursor placement matters during editing. It is not suitable for type="number" or type="date" fields, and adopters should first verify that their formatter preserves the order of characters matched by the global accept regular expression. Check the controlled value flow, mask behavior, required input props, and bundle expectations before adopting it.

## FAQ

### How do I install RIFM for a React project?

The README documents pnpm add rifm, npm install rifm, and yarn add rifm. RIFM has no runtime dependencies and declares React >=16.8 as a peer dependency.

### Can RIFM be used with a number input or date input?

No. The README says RIFM does not support type="number" or type="date" because those controls do not expose the selection APIs needed to restore the caret. Use type="text" with an appropriate inputMode instead.

### How does RIFM keep the cursor in the right place?

RIFM tracks characters matched by the global accept regular expression, which defaults to digits. The formatter may move separators, but it must preserve the order of those accepted characters so RIFM can locate the corresponding caret position.

### Does rifm/number support decimal and locale-specific input?

Yes. createNumberFormatter uses Intl.NumberFormat options and documents grouping, negative numbers, minimumFractionDigits, and maximumFractionDigits. It is string-based, so states such as 1. and 1.20 are preserved and large integers do not lose precision.

### What React version does RIFM require?

The README states that RIFM supports React 16.8 and newer. package.json declares the peer dependency as react >=16.8.

## Sources

- [License: MIT](https://github.com/TrySound/rifm/blob/main/LICENSE)
- [Project website](https://trysound.github.io/rifm/)
- [README](https://github.com/TrySound/rifm/blob/main/README.md)
- [Releases](https://github.com/TrySound/rifm/releases)
- [TrySound/rifm on GitHub](https://github.com/TrySound/rifm)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/trysound-rifm
