Migration From Manual React
Products
| # | Actions | Category | Status | ||
|---|---|---|---|---|---|
| 1 | SKU-001 | Starter Kit | hardware | active | |
| 2 | SKU-002 | Analytics Add-on | software | draft |
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
app/
products/
page.tsx
schemas/
products/
product.resource.schema.ts
components/
vibeflui/
renderers/
products/
ProductStatusBadge.tsx
lib/
vibeflui/
registry.tsx
examples/
product-crud-actions.tsBefore: manual React page
app/products/page.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
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
export function ProductStatusBadge({ value }: { value?: unknown }) {
return <span>{String(value ?? "-")}</span>;
}Registry file
lib/vibeflui/registry.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
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
"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.