При использовании библиотеки AutoNumeric в React возникает фундаментальная особенность: AutoNumeric напрямую изменяет DOM-элемент input, тогда как React стремится полностью контролировать состояние интерфейса через виртуальный DOM. Из-за этого появляются конфликты между реактивным обновлением состояния и внутренним форматированием библиотеки.
Наиболее стабильный подход — использование ref и работа
с экземпляром AutoNumeric внутри жизненного цикла компонента.
npm install autonumeric
или
yarn add autonumeric
import React, { useEffect, useRef } from 'react';
import AutoNumeric from 'autonumeric';
function PriceInput() {
const inputRef = useRef(null);
const anElement = useRef(null);
useEffect(() => {
anElement.current = new AutoNumeric(inputRef.current, {
currencySymbol: '₸ ',
decimalCharacter: ',',
digitGroupSeparator: ' ',
});
return () => {
anElement.current.remove();
};
}, []);
return (
<input
ref={inputRef}
type="text"
/>
);
}
export default PriceInput;
Следующий код создаёт конфликт:
<input
value={value}
onCha nge={(e) => setValue(e.target.value)}
/>
AutoNumeric самостоятельно форматирует содержимое поля:
1000 -> 1 000
Но React после обновления состояния пытается вернуть собственное значение в DOM. В результате появляются:
Для AutoNumeric рекомендуется использовать uncontrolled-компоненты.
import React, { useEffect, useRef } from 'react';
import AutoNumeric from 'autonumeric';
function AmountField() {
const inputRef = useRef(null);
useEffect(() => {
const an = new AutoNumeric(inputRef.current, {
decimalPlaces: 2,
});
return () => {
an.remove();
};
}, []);
return <input ref={inputRef} />;
}
React не управляет значением напрямую. Контроль осуществляется через API AutoNumeric.
const formatted = an.getFormatted();
Результат:
1 250,50
const numeric = an.getNumber();
Результат:
1250.5
const raw = an.getNumericString();
Результат:
1250.50
import React, { useEffect, useRef } from 'react';
import AutoNumeric from 'autonumeric';
function ProductPrice() {
const inputRef = useRef(null);
const autoNumericRef = useRef(null);
useEffect(() => {
autoNumericRef.current = new AutoNumeric(inputRef.current, {
currencySymbol: '$',
decimalPlaces: 2,
});
return () => {
autoNumericRef.current.remove();
};
}, []);
const handleClick = () => {
console.log(
autoNumericRef.current.getNumber()
);
};
return (
<div>
<input ref={inputRef} />
<button onCl ick={handleClick}>
Получить значение
</button>
</div>
);
}
export default ProductPrice;
Иногда необходимо хранить значение в React state.
import React, {
useEffect,
useRef,
useState
} from 'react';
import AutoNumeric from 'autonumeric';
function SalaryInput() {
const inputRef = useRef(null);
const anRef = useRef(null);
const [salary, setSalary] = useState('');
useEffect(() => {
anRef.current = new AutoNumeric(
inputRef.current,
{
currencySymbol: '€ ',
decimalPlaces: 2,
}
);
inputRef.current.addEventListener(
'autoNumeric:rawValueModified',
updateValue
);
return () => {
inputRef.current.removeEventListener(
'autoNumeric:rawValueModified',
updateValue
);
anRef.current.remove();
};
}, []);
const updateValue = () => {
setSalary(
anRef.current.getNumericString()
);
};
return (
<div>
<input ref={inputRef} />
<p>{salary}</p>
</div>
);
}
export default SalaryInput;
Библиотека генерирует собственные DOM-события.
| Событие | Назначение |
|---|---|
autoNumeric:formatted |
значение отформатировано |
autoNumeric:rawValueModified |
изменено raw-значение |
autoNumeric:minExceeded |
значение меньше минимума |
autoNumeric:maxExceeded |
значение больше максимума |
autoNumeric:invalidValue |
введено некорректное значение |
useEffect(() => {
const an = new AutoNumeric(inputRef.current);
const onFormat ted = (event) => {
console.log(event);
};
inputRef.current.addEventListener(
'autoNumeric:formatted',
onFormatted
);
return () => {
inputRef.current.removeEventListener(
'autoNumeric:formatted',
onFormatted
);
an.remove();
};
}, []);
an.set(15000);
const setPrice = () => {
anRef.current.set(9999.99);
};
an.update({
currencySymbol: '₽ ',
decimalCharacter: ',',
});
const switchCurrency = () => {
anRef.current.update({
currencySymbol: '€ ',
});
};
import React, {
useEffect,
useRef
} from 'react';
import AutoNumeric from 'autonumeric';
function MoneyInput({
currencySymbol,
decimalPlaces,
}) {
const inputRef = useRef(null);
const anRef = useRef(null);
useEffect(() => {
anRef.current = new AutoNumeric(
inputRef.current,
{
currencySymbol,
decimalPlaces,
}
);
return () => {
anRef.current.remove();
};
}, []);
useEffect(() => {
if (!anRef.current) {
return;
}
anRef.current.update({
currencySymbol,
decimalPlaces,
});
}, [currencySymbol, decimalPlaces]);
return (
<input ref={inputRef} />
);
}
export default MoneyInput;
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef
} from 'react';
import AutoNumeric from 'autonumeric';
const NumericInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
const anRef = useRef(null);
useEffect(() => {
anRef.current = new AutoNumeric(
inputRef.current,
props.options
);
return () => {
anRef.current.remove();
};
}, []);
useImperativeHandle(ref, () => ({
getValue() {
return anRef.current.getNumber();
},
setValue(value) {
anRef.current.set(value);
},
clear() {
anRef.current.clear();
}
}));
return <input ref={inputRef} />;
});
export default NumericInput;
import React, { useRef } from 'react';
import NumericInput from './NumericInput';
function App() {
const numericRef = useRef(null);
const showValue = () => {
console.log(
numericRef.current.getValue()
);
};
return (
<div>
<NumericInput
ref={numericRef}
options={{
currencySymbol: '$',
}}
/>
<button onCl ick={showValue}>
Показать
</button>
</div>
);
}
npm install react-hook-form
import React, {
useEffect,
useRef
} from 'react';
import { useForm, Controller }
from 'react-hook-form';
import AutoNumeric from 'autonumeric';
function Form() {
const { control, handleSubmit } =
useForm();
const onSub mit = (data) => {
console.log(data);
};
return (
<form onSub mit={handleSubmit(onSubmit)}>
<Controller
name="price"
control={control}
defaultValue=""
render={({ field }) => (
<AutoNumericField
field={field}
/>
)}
/>
<button type="submit">
Отправить
</button>
</form>
);
}
function AutoNumericField({ field }) {
const inputRef = useRef(null);
const anRef = useRef(null);
useEffect(() => {
anRef.current = new AutoNumeric(
inputRef.current,
{
currencySymbol: '$ ',
}
);
const updateValue = () => {
field.onChange(
anRef.current.getNumericString()
);
};
inputRef.current.addEventListener(
'autoNumeric:rawValueModified',
updateValue
);
return () => {
inputRef.current.removeEventListener(
'autoNumeric:rawValueModified',
updateValue
);
anRef.current.remove();
};
}, []);
return <input ref={inputRef} />;
}
import React, {
useEffect,
useRef
} from 'react';
import { Formik, Form, Field }
from 'formik';
import AutoNumeric from 'autonumeric';
function NumericField({ field }) {
const inputRef = useRef(null);
useEffect(() => {
const an = new AutoNumeric(
inputRef.current,
{
currencySymbol: '₽ ',
}
);
const sync = () => {
field.onChange({
target: {
name: field.name,
value: an.getNumericString(),
}
});
};
inputRef.current.addEventListener(
'autoNumeric:rawValueModified',
sync
);
return () => {
an.remove();
};
}, []);
return <input ref={inputRef} />;
}
function App() {
return (
<Formik
initialValues={{
amount: '',
}}
onSub mit={(values) => {
console.log(values);
}}
>
<Form>
<Field
name="amount"
component={NumericField}
/>
<button type="submit">
Submit
</button>
</Form>
</Formik>
);
}
import {
useEffect,
useRef
} from 'react';
import AutoNumeric from 'autonumeric';
export function useAutoNumeric(options) {
const inputRef = useRef(null);
const anRef = useRef(null);
useEffect(() => {
anRef.current = new AutoNumeric(
inputRef.current,
options
);
return () => {
anRef.current.remove();
};
}, []);
return {
inputRef,
autoNumeric: anRef,
};
}
import React from 'react';
import { useAutoNumeric }
from './useAutoNumeric';
function PaymentInput() {
const {
inputRef,
autoNumeric
} = useAutoNumeric({
currencySymbol: '$',
decimalPlaces: 2,
});
const show = () => {
console.log(
autoNumeric.current.getNumber()
);
};
return (
<div>
<input ref={inputRef} />
<button onCl ick={show}>
Value
</button>
</div>
);
}
В React 18 StrictMode в режиме разработки дважды
вызывает эффекты.
Из-за этого возможно:
useEffect(() => {
if (anRef.current) {
return;
}
anRef.current = new AutoNumeric(
inputRef.current
);
return () => {
anRef.current.remove();
anRef.current = null;
};
}, []);
AutoNumeric зависит от DOM и объекта window, поэтому при
SSR возникают ошибки:
window is not defined
import dynamic from 'next/dynamic';
const NumericField = dynamic(
() => import('../components/NumericField'),
{
ssr: false,
}
);
export default function Page() {
return <NumericField />;
}
useEffect(() => {
async function init() {
const AutoNumeric =
(await import('autonumeric')).default;
anRef.current = new AutoNumeric(
inputRef.current
);
}
init();
}, []);
AutoNumeric создаёт внутренние обработчики событий и хранит ссылки на DOM-элементы.
При размонтировании необходимо обязательно вызывать:
an.remove();
Без очистки возможны:
function Products({ items }) {
return (
<>
{items.map((item) => (
<PriceField
key={item.id}
value={item.price}
/>
))}
</>
);
}
Каждый компонент обязан иметь:
ref;const an = new AutoNumeric('.price');
Такой подход нарушает модель React, поскольку библиотека начинает самостоятельно управлять несколькими DOM-узлами вне компонентной структуры.
Крупные приложения обычно создают отдельный UI-компонент:
components/
NumericInput/
NumericInput.jsx
NumericInput.module.css
index.js
Внутри компонента скрывается:
Это позволяет использовать AutoNumeric как обычный React-компонент.
import React, {
useEffect,
useRef
} from 'react';
import AutoNumeric from 'autonumeric';
interface Props {
currencySymbol?: string;
}
export default function NumericInput({
currencySymbol = '$',
}: Props) {
const inputRef =
useRef<HTMLInputElement | null>(null);
const anRef =
useRef<AutoNumeric | null>(null);
useEffect(() => {
if (!inputRef.current) {
return;
}
anRef.current = new AutoNumeric(
inputRef.current,
{
currencySymbol,
}
);
return () => {
anRef.current?.remove();
};
}, []);
return <input ref={inputRef} />;
}
export interface NumericInputRef {
getValue(): number | null;
setValue(value: number): void;
}
При большом количестве полей важно избегать:
export default React.memo(NumericInput);
const handleChange = useCallback(() => {
console.log(
anRef.current.getNumber()
);
}, []);
Плохо:
const [formatted, setFormatted] =
useState('');
Лучше:
const value =
anRef.current.getNumericString();
В крупных React-проектах AutoNumeric обычно используется:
Типичная схема:
UI-компонент
↓
React Form Layer
↓
AutoNumeric Wrapper
↓
AutoNumeric Instance
↓
DOM Input
Такой подход позволяет изолировать низкоуровневую работу библиотеки от остального React-приложения.