VVibeFlui

Migration From Manual React

Generated UI previewTABLE + FORM

Products

#ActionsCategoryStatus
1
SKU-001Starter Kithardwareactive
2
SKU-002Analytics Add-onsoftwaredraft

This is the schema-driven result of the "Before" page above, backed by the same local React state — search, sort, create, edit, and the delete confirm dialog all come from the schema instead of hand-written JSX.

This example shows how to migrate a hand-written React table and form into VibeFlui.

Use this when an app already has working UI but the code is becoming repetitive: table columns, form fields, action buttons, validation, and local CRUD state are all mixed in one page component.

File structure

TXT
app/
  products/
    page.tsx
schemas/
  products/
    product.resource.schema.ts
components/
  vibeflui/
    renderers/
      products/
        ProductStatusBadge.tsx
lib/
  vibeflui/
    registry.tsx
    examples/
      product-crud-actions.ts

Before: manual React page

app/products/page.tsx

TSX
"use client";

import { useState } from "react";

type Product = {
  id: string;
  sku: string;
  name: string;
  category: string;
  status: string;
};

const initialProducts: Product[] = [
  { id: "prd_1", sku: "SKU-001", name: "Starter Kit", category: "hardware", status: "active" },
  { id: "prd_2", sku: "SKU-002", name: "Analytics Add-on", category: "software", status: "draft" }
];

export default function ProductsPage() {
  const [products, setProducts] = useState(initialProducts);
  const [name, setName] = useState("");

  const handleCreateProduct = () => {
    setProducts((current) => [
      ...current,
      {
        id: crypto.randomUUID(),
        sku: `SKU-${current.length + 1}`,
        name,
        category: "hardware",
        status: "draft"
      }
    ]);
    setName("");
  };

  return (
    <main>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          handleCreateProduct();
        }}
      >
        <input value={name} onChange={(event) => setName(event.target.value)} />
        <button type="submit">Create product</button>
      </form>

      <table>
        <thead>
          <tr>
            <th>SKU</th>
            <th>Name</th>
            <th>Category</th>
            <th>Status</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {products.map((product) => (
            <tr key={product.id}>
              <td>{product.sku}</td>
              <td>{product.name}</td>
              <td>{product.category}</td>
              <td>{product.status}</td>
              <td>
                <button
                  type="button"
                  onClick={() =>
                    setProducts((current) => current.filter((item) => item.id !== product.id))
                  }
                >
                  Delete
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </main>
  );
}

Migration target

Move repeated UI structure into a VibeFlui schema:

  • table columns become table.columns
  • form inputs become form.fields
  • delete confirmation becomes actions.delete.confirm
  • custom status display becomes a renderer component
  • local CRUD operations become reusable action helpers
  • the page only wires schema, data, registry, and handlers

Schema file

schemas/products/product.resource.schema.ts

TS
export const productResourceSchema = {
  resource: "products",
  title: "Products",
  mode: "static",
  identity: {
    key: "id",
    param: "id"
  },
  table: {
    columns: [
      { key: "sku", label: "SKU", searchable: true, sortable: true },
      { key: "name", label: "Name", searchable: true, sortable: true },
      { key: "category", label: "Category" },
      { key: "status", label: "Status", renderer: "ProductStatusBadge" }
    ],
    search: {
      enabled: true,
      placeholder: "Search products..."
    }
  },
  form: {
    mode: "modal",
    fields: [
      { name: "sku", label: "SKU", required: true },
      { name: "name", label: "Name", required: true, minLength: 2 },
      {
        name: "category",
        label: "Category",
        type: "select",
        options: [
          { label: "Hardware", value: "hardware" },
          { label: "Software", value: "software" },
          { label: "Service", value: "service" }
        ]
      },
      {
        name: "status",
        label: "Status",
        type: "select",
        defaultValue: "draft",
        options: [
          { label: "Draft", value: "draft" },
          { label: "Active", value: "active" },
          { label: "Discontinued", value: "discontinued" }
        ]
      }
    ]
  },
  actions: {
    create: { enabled: true, label: "Create product" },
    edit: { enabled: true },
    delete: {
      enabled: true,
      variant: "danger",
      confirm: {
        title: "Delete product?",
        description: "This removes the product from the local list."
      }
    }
  },
  registry: {
    renderers: ["ProductStatusBadge"]
  }
} as const;

Renderer component

components/vibeflui/renderers/products/ProductStatusBadge.tsx

TSX
export function ProductStatusBadge({ value }: { value?: unknown }) {
  return <span>{String(value ?? "-")}</span>;
}

Registry file

lib/vibeflui/registry.tsx

TSX
import { createRegistry } from "@vibeflui/core";
import { ProductStatusBadge } from "@/components/vibeflui/renderers/products/ProductStatusBadge";

export const vibefluiRegistry = createRegistry({
  renderers: {
    ProductStatusBadge
  }
});

Reusable action helpers

lib/vibeflui/examples/product-crud-actions.ts

TS
import type { Dispatch, SetStateAction } from "react";
import type { FluiKitSubmitContext } from "@vibeflui/core";

export type ProductRow = {
  id: string;
  sku: string;
  name: string;
  category: string;
  status: string;
};

export const initialProducts: ProductRow[] = [
  { id: "prd_1", sku: "SKU-001", name: "Starter Kit", category: "hardware", status: "active" },
  { id: "prd_2", sku: "SKU-002", name: "Analytics Add-on", category: "software", status: "draft" }
];

export function createProductCrudActions(setProducts: Dispatch<SetStateAction<ProductRow[]>>) {
  return {
    createProduct: async (values: Record<string, unknown>) => {
      const product = mapValuesToProduct(values);
      setProducts((current) => [...current, product]);
      return { status: true, code: 200, message: "Product created" };
    },
    updateProduct: async (values: Record<string, unknown>, context: FluiKitSubmitContext) => {
      const id = String(context.row?.id ?? "");
      const product = mapValuesToProduct(values, id);
      setProducts((current) => current.map((item) => (item.id === id ? product : item)));
      return { status: true, code: 200, message: "Product updated" };
    },
    deleteProduct: async (row: Record<string, unknown>) => {
      const id = String(row.id ?? "");
      setProducts((current) => current.filter((item) => item.id !== id));
      return { status: true, code: 200, message: "Product deleted" };
    }
  };
}

function mapValuesToProduct(values: Record<string, unknown>, id = crypto.randomUUID()): ProductRow {
  return {
    id,
    sku: String(values.sku ?? ""),
    name: String(values.name ?? ""),
    category: String(values.category ?? "hardware"),
    status: String(values.status ?? "draft")
  };
}

After: VibeFlui page

app/products/page.tsx

TSX
"use client";

import { useState } from "react";
import { FluiKit, FluiKitProvider } from "@vibeflui/core";
import { productResourceSchema } from "@/schemas/products/product.resource.schema";
import { vibefluiRegistry } from "@/lib/vibeflui/registry";
import {
  createProductCrudActions,
  initialProducts
} from "@/lib/vibeflui/examples/product-crud-actions";

export default function ProductsPage() {
  const [products, setProducts] = useState(initialProducts);
  const productActions = createProductCrudActions(setProducts);

  return (
    <FluiKitProvider registry={vibefluiRegistry}>
      <FluiKit
        schema={productResourceSchema}
        data={products}
        onCreate={productActions.createProduct}
        onUpdate={productActions.updateProduct}
        onDelete={productActions.deleteProduct}
      />
    </FluiKitProvider>
  );
}

Migration checklist

  • Keep existing data shape first; rename fields only after the migration works.
  • Move table columns into table.columns.
  • Move form controls into form.fields.
  • Move custom cell UI into renderer components.
  • Move state mutation into reusable action helpers.
  • Keep registry as wiring only.
  • Keep backend validation and authorization on the backend.