Умный объект с отслеживанием изменений, подписками на события и управлением состояниями. Идеален для форм, настроек и любых данных, требующих контроля изменений.
- Отслеживание изменений полей объекта в реальном времени.
- Подписка на изменения конкретных полей или всего объекта.
- Коммит (фиксация) текущих значений как «эталонных».
- Откат к последним закоммиченным или изначальным значениям.
- Проверка статуса изменений для каждого поля.
- Сериализация в JSON с поддержкой вложенных
SmartObjectи дат (Date→ ISO‑строка). - Рекурсивная обработка вложенных умных объектов.
- Оптимизация уведомлений: уведомления отправляются только при фактическом изменении значения.
npm install smart-object-kit
# Или через yarn
yarn add smart-object-kitimport { SmartObject } from 'smart-object-kit'
const user = new SmartObject({
name: 'ilya',
firstname: 'burcev',
age: 24,
})
user.setValue('age', 25)
console.log(user.isChanged()) // { age: true }
user.rejectChanges()
console.log(user.getValue('age')) // 24
console.log(user.isChanged()) // falseСоздаёт новый умный объект. Начальные значения глубоко копируются и сохраняются как эталонные.
const settings = new SmartObject({ theme: 'dark', language: 'ru' })Возвращает текущее значение поля.
const name = user.getValue('name') // 'ilya'Задаёт полю новое значение. Уведомления отправляются только если значение действительно изменилось.
user.setValue('age', 25) // уведомит подписчиков
user.setValue('age', 25) // НЕ уведомит — значение не изменилосьПодписка на изменения. Возвращает функцию отписки.
// Подписка на конкретные поля
const unsub = user.subscribe(['name', 'age'], () => {
console.log('name или age изменились')
})
// Подписка на все изменения
const unsubAll = user.subscribe(undefined, () => {
console.log('Что-то изменилось')
})
// Отписка
unsub()Удаляет подписчиков.
// Удалить конкретный колбэк с конкретного поля
user.unsubscribe(['name'], myCallback)
// Удалить всех подписчиков поля 'age'
user.unsubscribe(['age'])
// Удалить вообще всех подписчиков
user.unsubscribe()Фиксирует текущие значения как «эталонные». После коммита откат вернёт к этим значениям.
user.setValue('name', 'Ilya')
user.commitChanges() // закоммитить все поля
// или
user.commitChanges(['name']) // закоммитить только 'name'Откатывает значения к последним закоммиченным (или изначальным). Вызывает уведомления для откатанных полей.
user.setValue('age', 99)
user.rejectChanges(['age']) // откатить только 'age'
// или
user.rejectChanges() // откатить всёВозвращает объект с изменёнными полями ({ fieldName: true }) или false, если ничего не изменено.
user.setValue('name', 'Alex')
console.log(user.isChanged()) // { name: true }
user.commitChanges()
console.log(user.isChanged()) // falseВозвращает JSON-совместимый объект. Вложенные SmartObject рекурсивно разворачиваются, Date → ISO-строка.
const obj = user.toJSONFormat()
// { name: 'ilya', firstname: 'burcev', age: 24, profession: { name: 'frontend developer', ... } }Сериализует объект в JSON-строку.
const json = user.toJSONString()
// '{"name":"ilya","firstname":"burcev","age":24,"profession":{"name":"frontend developer",...}}'Вложенные SmartObject автоматически учитываются при проверке изменений и сериализации.
const user = new SmartObject({
name: 'ilya',
profession: new SmartObject({
name: 'frontend developer',
experience: 5,
salary: 2000,
}),
})
// Изменение вложенного объекта
user.getValue('profession').setValue('salary', 3000)
console.log(user.getValue('profession').isChanged()) // { salary: true }
console.log(user.isChanged()) // { profession: true }SmartObject внутри массивов также корректно отслеживаются.
const board = new SmartObject({
columns: [
new SmartObject({ title: 'To Do', color: '#ccc' }),
new SmartObject({ title: 'In Progress', color: '#ff0' }),
new SmartObject({ title: 'Done', color: '#0f0' }),
],
})
const columns = board.getValue('columns')
columns[0].setValue('color', '#999')
console.log(columns[0].isChanged()) // { color: true }
console.log(board.isChanged()) // { columns: true }Объекты Date автоматически преобразуются в ISO-строку при сериализации.
const event = new SmartObject({
title: 'Конференция',
date: new Date('2026-07-22'),
})
console.log(event.toJSONFormat())
// { title: 'Конференция', date: '2026-07-22T00:00:00.000Z' }
console.log(event.toJSONString())
// '{"title":"Конференция","date":"2026-07-22T00:00:00.000Z"}'SmartObject полностью типизирован — ключи и значения выводятся из начального объекта.
interface UserSchema {
name: string
age: number
active: boolean
}
const user = new SmartObject<UserSchema>({
name: 'Ilya',
age: 24,
active: true,
})
user.setValue('name', 'Alex') // OK
user.setValue('age', 'test') // Ошибка типа: string не назначаем number
user.getValue('name') // тип: string
user.getValue('age') // тип: numberconst form = new SmartObject({
email: 'user@mail.com',
password: '',
remember: false,
})
form.setValue('password', '123456')
form.setValue('remember', true)
if (form.isChanged()) {
console.log('Форма изменена')
}
// Пользователь нажал «Отмена»
form.rejectChanges()
console.log(form.getValue('password')) // ''
console.log(form.getValue('remember')) // false
// Пользователь нажал «Сохранить»
form.setValue('password', 'newpass')
form.commitChanges() // фиксировали — теперь откат вернёт к этим значениямconst user = new SmartObject({ name: 'Ilya', age: 24 })
const onNameChange = user.subscribe(['name'], () => {
console.log('Имя:', user.getValue('name'))
})
const onAnyChange = user.subscribe(undefined, () => {
console.log('Что-то изменилось')
})
user.setValue('name', 'Alex')
// → "Имя: Alex"
// → "Что-то изменилось"
user.setValue('age', 25)
// → "Что-то изменилось" (подписчик 'name' не вызывается)
onNameChange()
user.setValue('name', 'Oleg')
// → "Что-то изменилось" (только глобальный подписчик)Базовый хук для реактивной привязки SmartObject к компоненту. Перерисовка происходит только при изменении указанных полей.
import { useState, useEffect } from 'react'
import { SmartObject } from 'smart-object-kit'
function useSmartObject<T extends Record<string, any>, K extends keyof T>(
smartObj: SmartObject<T>,
keys: K[]
): Pick<T, K> {
const [, setTick] = useState(0)
useEffect(() => {
const unsubscribe = smartObj.subscribe(keys, () => {
setTick(tick => tick + 1)
})
return () => unsubscribe()
}, [smartObj, keys.join(',')])
const result = {} as Pick<T, K>
keys.forEach(key => {
result[key] = smartObj.getValue(key)
})
return result
}interface UserSchema {
name: string
age: number
email: string
}
const userStore = new SmartObject<UserSchema>({
name: 'Ivan',
age: 25,
email: 'ivan@mail.com',
})
function UserForm() {
const { name, age, email } = useSmartObject(userStore, ['name', 'age', 'email'])
const changes = userStore.isChanged()
const handleChange = (field: keyof UserSchema, value: string) => {
if (field === 'age') {
userStore.setValue(field, Number(value))
} else {
userStore.setValue(field, value)
}
}
return (
<div>
<input
value={name}
onChange={e => handleChange('name', e.target.value)}
/>
<input
value={String(age)}
onChange={e => handleChange('age', e.target.value)}
/>
<input
value={email}
onChange={e => handleChange('email', e.target.value)}
/>
{changes && <span style={{ color: 'orange' }}>Есть несохранённые изменения</span>}
<button onClick={() => userStore.commitChanges()}>Сохранить</button>
<button onClick={() => userStore.rejectChanges()} disabled={!changes}>
Отменить
</button>
</div>
)
}Каждый компонент подписывается только на нужные поля, что предотвращает лишние перерисовки.
const appStore = new SmartObject({
theme: 'dark',
language: 'ru',
userName: 'Ilya',
notifications: 3,
})
function ThemeToggle() {
const { theme } = useSmartObject(appStore, ['theme'])
return (
<button onClick={() => appStore.setValue('theme', theme === 'dark' ? 'light' : 'dark')}>
Тема: {theme}
</button>
)
}
function NotificationBadge() {
const { notifications } = useSmartObject(appStore, ['notifications'])
return <span>Уведомления: {notifications}</span>
}
function UserProfile() {
const { userName } = useSmartObject(appStore, ['userName'])
return <p>Пользователь: {userName}</p>
}
// Изменение theme НЕ вызывает перерисовку NotificationBadge и UserProfile
appStore.setValue('theme', 'light')const userStore = new SmartObject({
name: 'Ilya',
settings: new SmartObject({
theme: 'dark',
language: 'ru',
}),
})
function SettingsPanel() {
const settings = userStore.getValue('settings')
const { theme } = useSmartObject(settings, ['theme'])
return (
<div>
<p>Тема: {theme}</p>
<button onClick={() => settings.setValue('theme', theme === 'dark' ? 'light' : 'dark')}>
Переключить
</button>
</div>
)
}function useFormWithUndo<T extends Record<string, any>>(
store: SmartObject<T>,
keys: (keyof T)[]
) {
const values = useSmartObject(store, keys)
const changes = store.isChanged()
const save = () => store.commitChanges()
const undo = () => store.rejectChanges()
return { values, changes, save, undo }
}
// Использование
function ProfileEditor() {
const store = new SmartObject({ name: 'Ilya', bio: 'Developer' })
const { values, changes, save, undo } = useFormWithUndo(store, ['name', 'bio'])
return (
<div>
<input
value={values.name}
onChange={e => store.setValue('name', e.target.value)}
/>
<textarea
value={values.bio}
onChange={e => store.setValue('bio', e.target.value)}
/>
{changes && <p style={{ color: 'orange' }}>Не сохранено</p>}
<button onClick={save}>Сохранить</button>
<button onClick={undo} disabled={!changes}>Отменить</button>
</div>
)
}MIT