YP

Palistor

An open-source MVVM framework for React. Screen behaviour goes into a config, data stays behind a resolver, and JSX is left as a thin rendering layer. Battle-tested in production on pali.rent and kvartly.com; the public API is still tightening up — versions 0.0.x.

MIT · React 19 · TypeScript

In production

  • pali.rentfleet-management SaaS
  • kvartly.comreal estate marketplace and listing management SaaS
The Palistor README on GitHub: two code examples one after another — the JSX View layer and the ViewModel declaration in the config.

Three layers

Most React screens tangle three unrelated concerns inside one component: how the screen behaves, where its data comes from and how it looks. As the screen grows they braid together until any change touches everything. Palistor pulls them apart.

01View — rendering only

The JSX layer with everything but rendering stripped out: markup, layout and styles. A component reads field state and spreads it into your inputs. It is primitive enough that there is nowhere left to make a mistake.

A markup example (View in MVVM terms)
function Form() {
  const form = useForm(store);

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

02ViewModel — the config

One tree describes values, validation, visibility, cross-field rules and lifecycle callbacks. No useEffect, no custom hooks, no context: the necessary complexity is pulled into a single object you can read top to bottom — and, unlike a tree of hooks, review in one pass.

A configuration example (ViewModel declaration in MVVM terms)
const store = new Palistor({
  config: {
    id: { value: "" },
    email: {
      value: "",
      isRequired: true,
    },
    phone: {
      value: "",
      isVisible: (v) => v.email !== "",
      resolve: {
        // getPhone is already the data layer.
        resolver: async (v) => { return await getPhone(v.id) }
      }
    },
  },
});

03Model — the data layer

Fetching and sending data stays outside Palistor: a plain fetch, a caching layer or a full offline layer — whatever you plug in through resolve. Palistor defines the interface to it, not the implementation, and caches what a resolver returns so the same data is not fetched twice.

Quick start

Install from the public npm registry
npm install palistor
# peer dependency: react ^19

01Describe the form

The config is declarative: field values, validation, visibility and lifecycle callbacks live in one tree. Create the store at module level.

Payment form config
import { Palistor } from "palistor";

export const paymentStore = new Palistor({
  config: {
    paymentType: {
      value: "card",
      label: "Payment method",
    },
    cardNumber: {
      value: "",
      label: "Card number",
      placeholder: "0000 0000 0000 0000",
      isVisible: (v) => v.paymentType === "card",
      isRequired: (v) => v.paymentType === "card",
      validate: (value, v) =>
        v.paymentType === "card" && value.length < 16
          ? "Enter 16 digits"
          : undefined,
    },
    passport: {
      isVisible: (v) => v.paymentType === "bank",
      number:    { value: "", label: "Passport number", isRequired: true },
      issueDate: { value: "", label: "Issue date" },
    },
    amount: { value: 0, label: "Amount", isRequired: true },
  },
  initialValues: { paymentType: "card" },
});

02Connect a component

useForm returns a typed tracking proxy. The component re-renders only when the nodes it actually read change — writing to a neighbouring field does nothing to it.

Payment form component
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}
      />

      {/* The config knows the Input's interface thanks to an adapter (fieldMapping) */}
      {form.cardNumber.isVisible && <Input {...form.cardNumber} />}

      {form.passport.isVisible && <PassportSection passport={form.passport} />}

      <Button type="submit" isLoading={form.submitting}>Pay</Button>
    </form>
  );
}

Features

Granular re-renders
A component subscribes only to the fields it read — nothing else triggers a re-render.
Computed field state
isVisible, isRequired, label and validation errors are recomputed automatically from the config.
Proxy API
Native syntax: form.email.value = x instead of dispatching actions.
Submit pipeline
beforeSubmit → validate → onSubmit → afterSubmit; errors surface after the first failed submit.
Async resolvers
Data loading with auto-tracked dependencies, retry, optimistic updates and React Suspense.
Lists & entities
Normalized entity registry, list proxy with add / remove / setItems, per-entity templates.
Flows
Step wizards via defineFlow / defineStep: navigation, branching, per-step validation.
Persist
Autosave to localStorage, sessionStorage or any custom driver — flow navigation included.

Where it fits

  • Onboarding, KYC, verification and questionnaires — branching multi-step wizards, conditional fields, step-by-step validation.
  • Checkouts and payment forms — conditional payment methods, cross-field rules, async loading.
  • Configurators, calculators and CPQ — computed fields and dependencies (price × quantity → tax → total) without a single useEffect.
  • CRUD entity editors and tables with inline editing — a normalized registry, list proxy, per-entity templates.
  • Schema-driven forms — the config is data, so it can be generated or served from the backend, including per-region variants.

Where it doesn't

  • Content sites — blogs, docs, marketing, SSG: SEO-first, static, almost no behaviour.
  • Graphics, canvas and realtime rendering — games, rich-text and diagram editors, maps, heavy visualizations: the complexity lives in rendering and the model does not split into layers.
  • Trivial UI — one search box, one button, a form with a couple of fields: the three-layer split is overhead that does not pay off.
  • Embeddable widgets where bundle size is critical — the library is not small.

Live demo

Every example from this page runs in the playground. Each tab is a separate feature of the framework and has its own deep link.

In short

What is Palistor?

An open-source MVVM framework for React under the MIT licence. It splits a screen into three layers: JSX that only renders, a config that holds all behaviour — values, validation, visibility, cross-field rules — and a data layer you plug in through resolve. Instead of a tree of useEffects and custom hooks you get one flat object.

How is it different from Redux, Zustand or React Hook Form?

Those each solve one thing: global state, or form state. Palistor sets the architecture of the whole screen — behaviour, data and rendering are pulled apart on purpose. State is computed in the store, outside React's render cycle, and a component gets a signal only for the fields it actually read, so re-renders are targeted instead of cascading.

Is it used in real projects?

Yes. Palistor runs in production on pali.rent, a fleet-management SaaS, and on kvartly.com, a real estate marketplace — both are live and open to check. It is battle-tested on the author's own products, while the public API is still tightening up, which is what the 0.0.x versions say.

Why is it convenient for AI-generated code?

AI is bad at architecture but great at filling declarative slots. Palistor removes the architecture task itself: what is left is filling in a config by the rules. Reviewing generated code then means reading one flat object instead of a tree of useEffects, so there is far less room for it to fall apart.

What licence is it under and how do I install it?

MIT. Install it from the public npm registry with npm install palistor; the peer dependency is react ^19. The same package is published to GitHub Packages under the scoped name @projectint/palistor, but the canonical name is palistor.

Need a screen — or a whole product — built this way? Palistor grew out of the production of SaaS platforms we design, ship and run ourselves.