Работа с React

При использовании библиотеки AutoNumeric в React возникает фундаментальная особенность: AutoNumeric напрямую изменяет DOM-элемент input, тогда как React стремится полностью контролировать состояние интерфейса через виртуальный DOM. Из-за этого появляются конфликты между реактивным обновлением состояния и внутренним форматированием библиотеки.

Наиболее стабильный подход — использование ref и работа с экземпляром AutoNumeric внутри жизненного цикла компонента.

Установка

npm install autonumeric

или

yarn add autonumeric

Базовое подключение через useRef

Простейший функциональный компонент

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;

Почему нельзя использовать обычный controlled input

Следующий код создаёт конфликт:

<input
    value={value}
    onCha nge={(e) => setValue(e.target.value)}
/>

AutoNumeric самостоятельно форматирует содержимое поля:

1000 -> 1 000

Но React после обновления состояния пытается вернуть собственное значение в DOM. В результате появляются:

  • прыгающий курсор;
  • двойное форматирование;
  • потеря позиции каретки;
  • циклические перерисовки;
  • некорректное отображение.

Правильная архитектура: uncontrolled input

Для 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;

Работа с useState

Синхронизация состояния React

Иногда необходимо хранить значение в 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;

События AutoNumeric в React

Библиотека генерирует собственные 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();
    };
}, []);

Изменение значения программно

Метод set

an.set(15000);

В React-компоненте

const setPrice = () => {
    anRef.current.set(9999.99);
};

Изменение настроек после инициализации

Метод update

an.update({
    currencySymbol: '₽ ',
    decimalCharacter: ',',
});

React-пример

const switchCurrency = () => {
    anRef.current.update({
        currencySymbol: '€ ',
    });
};

Динамические настройки через props

Компонент с параметрами

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;

Использование forwardRef

Экспорт методов наружу

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>
    );
}

Работа с React Hook Form

Установка

npm install react-hook-form

Интеграция через Controller

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} />;
}

Работа с Formik

Интеграция

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>
    );
}

Создание собственного React-хука

Универсальный useAutoNumeric

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>
    );
}

Проблема двойной инициализации в StrictMode

В React 18 StrictMode в режиме разработки дважды вызывает эффекты.

Из-за этого возможно:

  • двойное создание AutoNumeric;
  • повторная подписка на события;
  • утечки памяти;
  • дублирование обработчиков.

Защита от повторной инициализации

useEffect(() => {
    if (anRef.current) {
        return;
    }

    anRef.current = new AutoNumeric(
        inputRef.current
    );

    return () => {
        anRef.current.remove();
        anRef.current = null;
    };
}, []);

Серверный рендеринг и Next.js

AutoNumeric зависит от DOM и объекта window, поэтому при SSR возникают ошибки:

window is not defined

Решение через dynamic import

Next.js

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;
  • отдельный экземпляр AutoNumeric;
  • собственную очистку.

Ошибочная архитектура

const an = new AutoNumeric('.price');

Такой подход нарушает модель React, поскольку библиотека начинает самостоятельно управлять несколькими DOM-узлами вне компонентной структуры.


Инкапсуляция AutoNumeric-компонента

Крупные приложения обычно создают отдельный UI-компонент:

components/
    NumericInput/
        NumericInput.jsx
        NumericInput.module.css
        index.js

Внутри компонента скрывается:

  • инициализация;
  • очистка;
  • синхронизация;
  • события;
  • форматирование;
  • интеграция с формами.

Это позволяет использовать AutoNumeric как обычный React-компонент.


Типизация в TypeScript

Компонент с типами

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} />;
}

Типизация ref API

export interface NumericInputRef {
    getValue(): number | null;
    setValue(value: number): void;
}

Производительность

При большом количестве полей важно избегать:

  • повторной инициализации;
  • лишних обновлений состояния;
  • controlled inputs;
  • частых re-render.

Оптимизация через memo

export default React.memo(NumericInput);

Стабилизация callback-функций

const handleChange = useCallback(() => {
    console.log(
        anRef.current.getNumber()
    );
}, []);

Избежание лишнего state

Плохо:

const [formatted, setFormatted] =
    useState('');

Лучше:

const value =
    anRef.current.getNumericString();

Архитектура enterprise-приложений

В крупных React-проектах AutoNumeric обычно используется:

  • в финансовых системах;
  • CRM;
  • ERP;
  • бухгалтерских интерфейсах;
  • банковских панелях;
  • кассовых приложениях;
  • аналитических системах;
  • административных формах.

Типичная схема:

UI-компонент
    ↓
React Form Layer
    ↓
AutoNumeric Wrapper
    ↓
AutoNumeric Instance
    ↓
DOM Input

Такой подход позволяет изолировать низкоуровневую работу библиотеки от остального React-приложения.