Управление состоянием компонента

При использовании Cleave.js основная сложность заключается не в самом форматировании, а в согласовании внутреннего состояния приложения с тем, что отображается в поле ввода. Библиотека работает как слой трансформации между «сырыми» данными пользователя и их визуальным представлением, что автоматически порождает необходимость чётко разделять два вида состояния: исходное значение и форматированное представление.

Ключевой принцип работы заключается в том, что Cleave.js не является источником истины для данных приложения. Он управляет отображением, но не диктует структуру состояния. Истина должна находиться либо в DOM-инпуте (в неконтролируемом режиме), либо во внешнем состоянии (в React, Vue, Svelte и аналогичных системах).

Разделение rawValue и formattedValue

В основе архитектуры состояния при работе с Cleave.js лежит различие между двумя сущностями:

rawValue — значение без форматирования, пригодное для хранения, отправки на сервер и бизнес-логики.

formattedValue — отображаемая строка, содержащая маски, разделители, символы валюты и прочие визуальные элементы.

Cleave.js предоставляет доступ к обоим уровням через callback-механизм и методы экземпляра. Это позволяет строить предсказуемую модель данных, где UI не влияет на семантику значения.

Типичный сценарий обновления состояния выглядит следующим образом: пользователь вводит данные → Cleave.js преобразует ввод → вызывается событие обновления → приложение синхронизирует rawValue со своим состоянием → при необходимости пересчитывается UI.

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

При создании экземпляра Cleave.js состояние формируется из начального значения input-элемента. Если поле уже содержит данные, библиотека немедленно применяет правила форматирования и приводит значение к согласованному виду.

const cleave = new Cleave(inputElement, {
    numeral: true,
    numeralThousandsGroupStyle: 'thousand'
});

В этом сценарии важно учитывать, что первичная инициализация уже изменяет DOM. Это означает, что состояние внешнего приложения должно быть либо синхронизировано заранее, либо считано после инициализации.

Для систем с контролируемыми компонентами это особенно критично: значение в state и значение в input должны совпасть до подключения Cleave.js, иначе возможны скачки курсора или переформатирование на первом рендере.

Управление состоянием через onValueChanged

Основной механизм синхронизации — callback onValueChanged. Он предоставляет доступ к текущему состоянию поля в момент изменения.

const cleave = new Cleave(inputElement, {
    phone: true,
    onValueChanged: function (e) {
        const { rawValue, value } = e.target;

        appState.phoneRaw = rawValue;
        appState.phoneFormatted = value;
    }
});

Здесь происходит ключевое разделение ответственности:

  • Cleave.js отвечает за преобразование
  • приложение отвечает за хранение и реакцию на изменения

Важно учитывать, что value — это уже форматированная строка, а rawValue — нормализованное значение без маски. Именно rawValue обычно используется в бизнес-логике.

Неконтролируемое состояние и его особенности

В неконтролируемом режиме Cleave.js полностью управляет DOM-значением. Внешнее приложение не вмешивается в процесс ввода, а состояние извлекается только при необходимости, например при отправке формы.

const cleave = new Cleave(inputElement, {
    numeral: true
});

form.addEventListener('submit', () => {
    const value = cleave.getRawValue();
});

Такая модель снижает сложность синхронизации, но делает состояние «ленивым». Оно существует только в DOM до момента извлечения.

Недостаток подхода проявляется при необходимости реактивного поведения: изменение одного поля не влияет на другие части интерфейса без дополнительного ручного извлечения значения.

Контролируемое состояние в React-подобных системах

В средах с виртуальным DOM Cleave.js часто становится источником побочных эффектов, так как напрямую изменяет input. Поэтому управление состоянием требует строгой синхронизации.

Основная проблема — предотвращение конфликта между render-циклом и внутренним форматированием.

const [value, setValue] = useState('');

useEffect(() => {
    const cleave = new Cleave(inputRef.current, {
        numeral: true,
        onValueChanged: (e) => {
            setValue(e.target.rawValue);
        }
    });

    return () => cleave.destroy();
}, []);

В этой модели:

  • React хранит rawValue как источник истины
  • Cleave.js отвечает за отображение
  • обновления проходят через callback

Особенность заключается в том, что прямое обновление value из state в input может конфликтовать с форматированием Cleave.js, поэтому любые внешние изменения требуют осторожности.

Программное обновление состояния

Cleave.js предоставляет методы для изменения значения без пользовательского ввода. Это важно для сценариев автозаполнения, восстановления формы и динамических вычислений.

cleave.setRawValue('79991234567');

После вызова метода библиотека автоматически пересчитывает отображаемое значение.

Однако программное обновление состояния может привести к рассинхронизации, если внешнее приложение не обновляет свой state одновременно. Поэтому правило заключается в том, что любое изменение через API Cleave.js должно сопровождаться обновлением внешнего состояния.

Переключение конфигурации и пересоздание состояния

Состояние Cleave.js тесно связано с конфигурацией. Изменение опций (например, переход с телефонной маски на числовую или валютную) часто требует пересоздания экземпляра.

cleave.destroy();

const newCleave = new Cleave(inputElement, {
    numeral: true,
    numeralDecimalScale: 2
});

Причина в том, что внутренние алгоритмы форматирования зависят от набора правил, и частичное обновление может привести к неконсистентному состоянию.

При этом внешнее состояние должно быть сохранено отдельно:

  • rawValue извлекается до уничтожения экземпляра
  • затем передаётся новому экземпляру

Состояние курсора как часть модели данных

Хотя курсор формально не является частью данных, в Cleave.js он становится элементом состояния, поскольку форматирование напрямую влияет на его позицию.

Каждое изменение значения может смещать caret, особенно при вставке разделителей или изменении длины строки.

Внутренне библиотека пересчитывает позицию курсора после каждого обновления, но при внешнем управлении состоянием возможны конфликты:

  • повторное форматирование после setState
  • двойное применение маски
  • скачки позиции при ререндере

Поэтому важно избегать циклов «state → value → state», где каждое обновление инициирует новое форматирование.

Состояние при динамическом создании полей

В формах с динамическими полями (например, списки телефонов или банковских реквизитов) каждый экземпляр Cleave.js имеет собственное локальное состояние.

fields.forEach((field) => {
    new Cleave(field.element, field.options);
});

В таких сценариях состояние приложения становится коллекцией независимых значений. Ключевая задача — обеспечить стабильную идентификацию каждого экземпляра, чтобы при удалении или добавлении поля не происходило смешивания данных.

Очистка и сброс состояния

Сброс состояния требует согласованного удаления как DOM-значения, так и внутреннего состояния Cleave.js.

cleave.setRawValue('');

или при полном сбросе:

cleave.destroy();
input.value = '';

Важно учитывать, что уничтожение экземпляра не очищает внешнее состояние приложения. Если state не синхронизирован, после повторной инициализации возможно восстановление старого значения.

Хранение состояния в бизнес-логике

Практика хранения formattedValue как основного состояния считается ошибочной в большинстве сценариев. Форматированное значение предназначено исключительно для отображения.

Корректная модель хранения выглядит следующим образом:

  • rawValue хранится в state
  • formattedValue вычисляется на уровне UI
  • Cleave.js выступает в роли трансформера между ними

Такой подход упрощает валидацию, сериализацию и интеграцию с API, поскольку исключает зависимость от визуального представления данных.

Конфликты состояния при асинхронных обновлениях

Наиболее сложные проблемы возникают при асинхронных сценариях:

  • загрузка данных с сервера
  • автозаполнение
  • обновление формы через WebSocket
  • частичное восстановление состояния

Если новое значение устанавливается до завершения инициализации Cleave.js, оно может быть переформатировано некорректно или частично проигнорировано.

Поэтому последовательность обновлений состояния критична:

  1. инициализация DOM
  2. создание Cleave.js
  3. установка начального rawValue
  4. подключение реактивного слоя

Нарушение порядка приводит к дрейфу состояния между слоями приложения.

Инварианты состояния при использовании Cleave.js

При проектировании систем с Cleave.js важно поддерживать несколько устойчивых правил:

  • rawValue всегда первичен по отношению к formattedValue
  • экземпляр Cleave.js не является источником истины
  • любое изменение DOM должно отражаться во внешнем состоянии
  • конфигурация маски и данные должны синхронизироваться через пересоздание, а не частичное изменение
  • состояние курсора не должно учитываться в бизнес-логике

Соблюдение этих инвариантов позволяет избежать рассинхронизации между пользовательским вводом и моделью данных приложения, особенно в сложных формах с динамическими изменениями и реактивными обновлениями.