Palistor
Открытый MVVM-фреймворк для React. Поведение экрана уезжает в конфиг, данные остаются за резольвером, а JSX остаётся тонким слоем отображения. Обкатан в продакшене pali.rent и kvartly.com; публичный API ещё уплотняется — версии 0.0.x.
MIT · React 19 · TypeScript
В продакшене
- pali.rent — SaaS для управления автопарками
- kvartly.com — маркетплейс недвижимости и SaaS для управления объектами

Три слоя
Большинство React-экранов сплетают внутри одного компонента три несвязанные заботы: как экран себя ведёт, откуда берутся его данные и как он выглядит. По мере роста экрана они срастаются так, что любое изменение задевает всё. Palistor их разводит.
01View — только отображение
JSX-слой, из которого изъято всё, кроме собственно отображения: вёрстка, рендеринг, стили. Компонент читает состояние поля и раскладывает его в свои инпуты. Он настолько примитивен, что ошибиться там негде.
function Form() {
const form = useForm(store);
return (
<>
<Input {...form.email} />
{form.phone.isVisible && <Input {...form.phone} />}
</>
);
}02ViewModel — конфиг
Одно дерево описывает значения, валидацию, видимость, кросс-полевые правила и lifecycle-колбэки. Никаких useEffect, кастомных хуков и контекстов: вся необходимая сложность стянута в один объект, который читается сверху вниз — и, в отличие от дерева хуков, ревьюится за один проход.
const store = new Palistor({
config: {
id: { value: "" },
email: {
value: "",
isRequired: true,
},
phone: {
value: "",
isVisible: (v) => v.email !== "",
resolve: {
// getPhone это уже слой данных.
resolver: async (v) => { return await getPhone(v.id) }
}
},
},
});03Model — слой данных
Получение и отправка данных остаются вне Palistor: простой fetch, кэширующий слой или полноценный offline-слой — что угодно, что вы подключаете через resolve. Palistor задаёт интерфейс к нему, а не реализацию, и кеширует то, что вернул резольвер, чтобы не тянуть одни и те же данные дважды.
Быстрый старт
npm install palistor
# peer-зависимость: react ^1901Опишите форму
Конфиг декларативен: значения полей, валидация, видимость и lifecycle-колбэки живут в одном дереве. Store создаётся на уровне модуля.
import { Palistor } from "palistor";
export const paymentStore = new Palistor({
config: {
paymentType: {
value: "card",
label: "Способ оплаты",
},
cardNumber: {
value: "",
label: "Номер карты",
placeholder: "0000 0000 0000 0000",
isVisible: (v) => v.paymentType === "card",
isRequired: (v) => v.paymentType === "card",
validate: (value, v) =>
v.paymentType === "card" && value.length < 16
? "Введите 16 цифр"
: undefined,
},
passport: {
isVisible: (v) => v.paymentType === "bank",
number: { value: "", label: "Серия и номер", isRequired: true },
issueDate: { value: "", label: "Дата выдачи" },
},
amount: { value: 0, label: "Сумма", isRequired: true },
},
initialValues: { paymentType: "card" },
});02Подключите компонент
useForm возвращает типизированный tracking proxy. Компонент ре-рендерится только при изменении тех узлов, которые он реально прочитал, — запись в соседнее поле его не трогает.
import { useForm } from "palistor";
import { paymentStore } from "./paymentStore";
function PaymentForm() {
const form = useForm(paymentStore);
return (
<form onSubmit={(e) => { e.preventDefault(); paymentStore.submit(); }}>
<Select
value={form.paymentType.value}
onChange={(e) => (form.paymentType.value = e.target.value)}
label={form.paymentType.label}
/>
{/* Конфиг знает об интерфейсе Input благодаря адаптеру (fieldMapping) */}
{form.cardNumber.isVisible && <Input {...form.cardNumber} />}
{form.passport.isVisible && <PassportSection passport={form.passport} />}
<Button type="submit" isLoading={form.submitting}>Оплатить</Button>
</form>
);
}Возможности
- Гранулярные ре-рендеры
- Компонент подписывается только на те поля, которые он читал, — больше ничего не триггерит ре-рендер.
- Computed field state
- isVisible, isRequired, label и ошибки валидации пересчитываются автоматически по конфигу.
- Proxy API
- Нативный синтаксис: form.email.value = x вместо диспатча экшенов.
- Submit pipeline
- beforeSubmit → validate → onSubmit → afterSubmit; ошибки показываются после первого неудачного submit.
- Async-резолверы
- Загрузка данных с авто-трекингом зависимостей, retry, optimistic updates и React Suspense.
- Списки и сущности
- Нормализованный реестр сущностей, list proxy с add / remove / setItems, per-entity шаблоны.
- Flows
- Пошаговые мастера через defineFlow / defineStep: навигация, ветвление, валидация по шагам.
- Persist
- Автосохранение в localStorage, sessionStorage или любой кастомный драйвер — вместе с навигацией флоу.
Где применять
- Онбординг, KYC, верификация и анкеты — ветвящиеся многошаговые мастера, условные поля, пошаговая валидация.
- Чек-ауты и платёжные формы — условные способы оплаты, кросс-полевые правила, async-подгрузка.
- Конфигураторы, калькуляторы и CPQ — computed-поля и зависимости (цена × количество → налог → итог) без единого useEffect.
- CRUD-редакторы сущностей и таблицы с inline-правкой — нормализованный реестр, list-proxy, per-entity шаблоны.
- Schema-driven формы — конфиг это данные, поэтому его можно генерировать или отдавать с бэкенда, в том числе под разные регионы.
Где не стоит
- Контент-сайты — блоги, документация, маркетинг, SSG: SEO-first, статика, поведения почти нет.
- Графика, canvas и realtime-рендер — игры, редакторы rich-text и диаграмм, карты, тяжёлые визуализации: сложность живёт в рендере, а модель не расщепляется на слои.
- Тривиальный UI — один поиск, одна кнопка, форма из пары полей: трёхслойное разделение — оверхед, который не окупается.
- Встраиваемые виджеты, где критичен вес бандла, — библиотека не маленькая.
Живое демо
Все примеры с этой страницы работают на живом стенде. Каждый таб — отдельная возможность фреймворка, на каждый ведёт прямая ссылка.
- Быстрый старт — условные поля, submit-pipeline, persist
- Flows — пошаговый мастер с ветвлением
- Списки и сущности — нормализованный реестр, list-proxy, контекст store
- Async-резолверы — загрузка данных, retry, уведомления
- Field mapping — переименование пропсов под UI-кит
Коротко о главном
Что такое Palistor?
Открытый MVVM-фреймворк для React под лицензией MIT. Он делит экран на три слоя: JSX, который только отображает; конфиг, в котором лежит всё поведение — значения, валидация, видимость, кросс-полевые правила; и слой данных, который вы подключаете через resolve. Вместо дерева useEffect'ов и кастомных хуков получается один плоский объект.
Чем он отличается от Redux, Zustand или React Hook Form?
Каждый из них решает что-то одно: глобальное состояние или состояние формы. Palistor задаёт архитектуру экрана целиком — поведение, данные и отображение разнесены намеренно. Состояние считается в сторе, вне рендер-цикла React, и компонент получает сигнал только по тем полям, что реально прочитал, поэтому ре-рендеры точечные, а не каскадные.
Используется ли он в реальных проектах?
Да. Palistor работает в продакшене pali.rent — SaaS для управления автопарками — и kvartly.com, маркетплейса недвижимости; оба открыты по ссылке и их можно проверить. Он обкатан на собственных продуктах автора, при этом публичный API ещё уплотняется — об этом и говорят версии 0.0.x.
Почему это удобно для генерации кода ИИ?
ИИ плохо архитектурит, но отлично заполняет декларативные слоты. Palistor убирает саму задачу архитектуры: остаётся заполнить конфиг по правилам. Ревью сгенерированного кода превращается в чтение одного плоского объекта вместо дерева useEffect'ов, поэтому развалить его гораздо труднее.
Какая лицензия и как поставить?
MIT. Ставится из публичного npm-реестра командой npm install palistor, peer-зависимость — react ^19. Тот же пакет опубликован в GitHub Packages под scoped-именем @projectint/palistor, но каноническим считается имя palistor.
Нужен экран — или продукт целиком — построенный так же? Palistor вырос из продакшена SaaS-платформ, которые мы проектируем, запускаем и сопровождаем сами.