YP

Palistor

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

MIT · React 19 · TypeScript

В продакшене

  • pali.rentSaaS для управления автопарками
  • kvartly.comмаркетплейс недвижимости и SaaS для управления объектами
README Palistor на GitHub: два примера кода друг за другом — слой View на JSX и декларация ViewModel в конфиге.

Три слоя

Большинство React-экранов сплетают внутри одного компонента три несвязанные заботы: как экран себя ведёт, откуда берутся его данные и как он выглядит. По мере роста экрана они срастаются так, что любое изменение задевает всё. Palistor их разводит.

01View — только отображение

JSX-слой, из которого изъято всё, кроме собственно отображения: вёрстка, рендеринг, стили. Компонент читает состояние поля и раскладывает его в свои инпуты. Он настолько примитивен, что ошибиться там негде.

Пример вёрстки (View по MVVM)
function Form() {
  const form = useForm(store);

  return (
    <>
      <Input {...form.email} />
      {form.phone.isVisible && <Input {...form.phone} />}
    </>
  );
}

02ViewModel — конфиг

Одно дерево описывает значения, валидацию, видимость, кросс-полевые правила и lifecycle-колбэки. Никаких useEffect, кастомных хуков и контекстов: вся необходимая сложность стянута в один объект, который читается сверху вниз — и, в отличие от дерева хуков, ревьюится за один проход.

Пример конфигурации (декларация ViewModel по MVVM)
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-реестра
npm install palistor
# peer-зависимость: react ^19

01Опишите форму

Конфиг декларативен: значения полей, валидация, видимость и 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 — один поиск, одна кнопка, форма из пары полей: трёхслойное разделение — оверхед, который не окупается.
  • Встраиваемые виджеты, где критичен вес бандла, — библиотека не маленькая.

Живое демо

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

Коротко о главном

Что такое 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-платформ, которые мы проектируем, запускаем и сопровождаем сами.