Detail Page: Viewing and Editing Data
- Using
getOneto query a single record - Using
updateto update data - Using
deleteto delete data - Implementing view/edit mode switching
Estimated Time: 20 minutes
What Are We Building?
In the previous section, we implemented the customer list. Now let's build the detail page:
- Display complete information for a single customer
- Support editing (click to switch to edit mode)
- Support deletion (with confirmation)
- Return to the list after successful operations

Step 1: Configure Routes
The detail page needs to fetch data using the ID from the URL, so let's configure the route first.
// src/App.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import CustomerList from "./pages/customer-list";
import CustomerDetail from "./pages/customer-detail";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<CustomerList />} />
<Route path="/customer/:id" element={<CustomerDetail />} />
</Routes>
</BrowserRouter>
);
}
export default App;
/customer/:id-:idis a dynamic parameter that can be retrieved in the component usinguseParams()- When accessing
/customer/123,idwill be123
Step 2: Fetch Data Details
Create the detail page component and use getOne to fetch a single record:
// src/pages/customer-detail.tsx
/**
* Title: Customer Detail
*/
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { lovrabetClient } from "../api/client";
// Customer data type
interface Customer {
id: number;
name: string;
phone: string;
company: string;
status: "potential" | "active" | "lost";
create_time: string;
update_time: string;
}
export default function CustomerDetail() {
// Get customer ID from URL
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [customer, setCustomer] = useState<Customer | null>(null);
const [loading, setLoading] = useState(true);
// Fetch customer details
useEffect(() => {
async function fetchCustomer() {
if (!id) return;
try {
const data = await lovrabetClient.models.customers.getOne<Customer>(id);
setCustomer(data);
} catch (error) {
console.error("Failed to fetch data:", error);
alert("Data does not exist or has been deleted");
navigate("/");
} finally {
setLoading(false);
}
}
fetchCustomer();
}, [id]);
if (loading) {
return <div style={{ padding: 24 }}>Loading...</div>;
}
if (!customer) {
return <div style={{ padding: 24 }}>Data does not exist</div>;
}
return (
<div style={{ padding: 24 }}>
<button onClick={() => navigate("/")}>← Back to List</button>
<h1>Customer Details</h1>
<div style={{ marginTop: 16 }}>
<p><strong>Name:</strong>{customer.name}</p>
<p><strong>Phone:</strong>{customer.phone}</p>
<p><strong>Company:</strong>{customer.company || "-"}</p>
<p><strong>Status:</strong>{customer.status}</p>
<p><strong>Created At:</strong>{customer.create_time}</p>
</div>
</div>
);
}
useParams()- Retrieves the:idparameter from the URLuseNavigate()- Programmatic navigation for page transitions.getOne<Customer>(id)- Fetches a single record by ID, supports genericsgetOnereturns the data object directly, not a{ data: ... }wrapper structure
Step 3: Add Edit Functionality
Now let users edit customer information. We'll implement this by switching between "view mode" and "edit mode":
// src/pages/customer-detail.tsx
/**
* Title: Customer Detail
*/
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { lovrabetClient } from "../api/client";
interface Customer {
id: number;
name: string;
phone: string;
company: string;
status: "potential" | "active" | "lost";
create_time: string;
}
const STATUS_OPTIONS = [
{ value: "potential", label: "Potential Customer" },
{ value: "active", label: "Active Customer" },
{ value: "lost", label: "Lost Customer" },
];
export default function CustomerDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [customer, setCustomer] = useState<Customer | null>(null);
const [loading, setLoading] = useState(true);
// New: Edit-related state
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState<Partial<Customer>>({});
const [saving, setSaving] = useState(false);
// Fetch customer details
useEffect(() => {
async function fetchCustomer() {
if (!id) return;
try {
const data = await lovrabetClient.models.customers.getOne<Customer>(id);
setCustomer(data);
setEditForm(data); // Initialize edit form
} catch (error) {
console.error("Failed to fetch data:", error);
alert("Data does not exist");
navigate("/");
} finally {
setLoading(false);
}
}
fetchCustomer();
}, [id]);
// New: Save edits
const handleSave = async () => {
if (!id) return;
setSaving(true);
try {
// Call update API
const updated = await lovrabetClient.models.customers.update(id, editForm);
setCustomer(updated); // Update display data
setIsEditing(false); // Exit edit mode
alert("Saved successfully!");
} catch (error) {
console.error("Failed to save:", error);
alert("Failed to save, please try again");
} finally {
setSaving(false);
}
};
// New: Cancel editing
const handleCancel = () => {
setEditForm(customer || {}); // Restore original data
setIsEditing(false);
};
if (loading) return <div style={{ padding: 24 }}>Loading...</div>;
if (!customer) return <div style={{ padding: 24 }}>Data does not exist</div>;
return (
<div style={{ padding: 24 }}>
<button onClick={() => navigate("/")}>← Back to List</button>
<h1>Customer Details</h1>
{isEditing ? (
// Edit mode
<div style={{ marginTop: 16 }}>
<div style={formGroupStyle}>
<label style={labelStyle}>Name</label>
<input
style={inputStyle}
value={editForm.name || ""}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
/>
</div>
<div style={formGroupStyle}>
<label style={labelStyle}>Phone</label>
<input
style={inputStyle}
value={editForm.phone || ""}
onChange={(e) => setEditForm({ ...editForm, phone: e.target.value })}
/>
</div>
<div style={formGroupStyle}>
<label style={labelStyle}>Company</label>
<input
style={inputStyle}
value={editForm.company || ""}
onChange={(e) => setEditForm({ ...editForm, company: e.target.value })}
/>
</div>
<div style={formGroupStyle}>
<label style={labelStyle}>Status</label>
<select
style={inputStyle}
value={editForm.status || ""}
onChange={(e) => setEditForm({ ...editForm, status: e.target.value as any })}
>
{STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div style={{ marginTop: 24 }}>
<button onClick={handleSave} disabled={saving} style={{ marginRight: 8 }}>
{saving ? "Saving..." : "Save"}
</button>
<button onClick={handleCancel} disabled={saving}>
Cancel
</button>
</div>
</div>
) : (
// View mode
<div style={{ marginTop: 16 }}>
<p><strong>Name:</strong>{customer.name}</p>
<p><strong>Phone:</strong>{customer.phone}</p>
<p><strong>Company:</strong>{customer.company || "-"}</p>
<p><strong>Status:</strong>{
STATUS_OPTIONS.find(o => o.value === customer.status)?.label
}</p>
<p><strong>Created At:</strong>{
new Date(customer.create_time).toLocaleString()
}</p>
<div style={{ marginTop: 24 }}>
<button onClick={() => setIsEditing(true)} style={{ marginRight: 8 }}>
Edit
</button>
</div>
</div>
)}
</div>
);
}
// Simple styles
const formGroupStyle: React.CSSProperties = { marginBottom: 16 };
const labelStyle: React.CSSProperties = { display: "block", marginBottom: 4, fontWeight: "bold" };
const inputStyle: React.CSSProperties = { padding: 8, width: 300 };
update(id, data)- Updates the record with the specified ID- Only pass the fields that need to be modified, no need to pass all fields
- Returns the complete updated data (including auto-updated
update_time) - Use a separate
editFormstate for editing; cancel restores original data to avoid polluting the display
Step 4: Add Delete Functionality
Delete operations should be handled carefully, add a confirmation dialog:
// Add to CustomerDetail component
// Delete customer
const handleDelete = async () => {
if (!id) return;
// Confirmation dialog
const confirmed = window.confirm("Are you sure you want to delete this customer? This action cannot be undone!");
if (!confirmed) return;
try {
await lovrabetClient.models.customers.delete(id);
alert("Deleted successfully!");
navigate("/"); // Return to list
} catch (error) {
console.error("Failed to delete:", error);
alert("Failed to delete, please try again");
}
};
// Add delete button in JSX
<button onClick={handleDelete} style={{ marginLeft: 8, color: "red" }}>
Delete
</button>
Delete functionality is only available in WebAPI mode (Cookie authentication).
If you're using OpenAPI mode (AccessKey authentication), delete operations will result in an error. In this case, consider using "soft delete" — add a deleted field to mark the deletion status.
Step 5: Add Navigation from List Page
Finally, let each row in the list page be clickable to navigate to the detail page:
// Modify in src/pages/customer-list.tsx
import { useNavigate } from "react-router-dom";
export default function CustomerList() {
const navigate = useNavigate();
// ... other code ...
return (
<table>
<tbody>
{customers.map((customer) => (
<tr
key={customer.id}
onClick={() => navigate(`/customer/${customer.id}`)}
style={{ cursor: "pointer" }} // Change cursor to pointer
>
<td>{customer.name}</td>
<td>{customer.phone}</td>
{/* ... */}
</tr>
))}
</tbody>
</table>
);
}
Complete Code
Click to view complete code
// src/pages/customer-detail.tsx
/**
* Title: Customer Detail
*/
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { lovrabetClient } from "../api/client";
interface Customer {
id: number;
name: string;
phone: string;
company: string;
status: "potential" | "active" | "lost";
create_time: string;
update_time: string;
}
const STATUS_OPTIONS = [
{ value: "potential", label: "Potential Customer" },
{ value: "active", label: "Active Customer" },
{ value: "lost", label: "Lost Customer" },
];
export default function CustomerDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [customer, setCustomer] = useState<Customer | null>(null);
const [loading, setLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState<Partial<Customer>>({});
const [saving, setSaving] = useState(false);
// Fetch data
useEffect(() => {
async function fetchCustomer() {
if (!id) return;
setLoading(true);
try {
const data = await lovrabetClient.models.customers.getOne<Customer>(id);
setCustomer(data);
setEditForm(data);
} catch (error) {
console.error("Failed to fetch data:", error);
alert("Data does not exist");
navigate("/");
} finally {
setLoading(false);
}
}
fetchCustomer();
}, [id]);
// Save
const handleSave = async () => {
if (!id) return;
setSaving(true);
try {
const updated = await lovrabetClient.models.customers.update(id, editForm);
setCustomer(updated);
setIsEditing(false);
alert("Saved successfully!");
} catch (error) {
console.error("Failed to save:", error);
alert("Failed to save, please try again");
} finally {
setSaving(false);
}
};
// Cancel editing
const handleCancel = () => {
setEditForm(customer || {});
setIsEditing(false);
};
// Delete
const handleDelete = async () => {
if (!id) return;
if (!window.confirm("Are you sure you want to delete? This action cannot be undone!")) return;
try {
await lovrabetClient.models.customers.delete(id);
alert("Deleted successfully!");
navigate("/");
} catch (error) {
console.error("Failed to delete:", error);
alert("Failed to delete, please try again");
}
};
if (loading) return <div style={{ padding: 24 }}>Loading...</div>;
if (!customer) return <div style={{ padding: 24 }}>Data does not exist</div>;
const getStatusLabel = (status: string) =>
STATUS_OPTIONS.find(o => o.value === status)?.label || status;
return (
<div style={{ padding: 24, maxWidth: 600 }}>
<button onClick={() => navigate("/")}>← Back to List</button>
<h1>{isEditing ? "Edit Customer" : "Customer Details"}</h1>
{isEditing ? (
<div style={{ marginTop: 16 }}>
<FormField label="Name">
<input
value={editForm.name || ""}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
/>
</FormField>
<FormField label="Phone">
<input
value={editForm.phone || ""}
onChange={(e) => setEditForm({ ...editForm, phone: e.target.value })}
/>
</FormField>
<FormField label="Company">
<input
value={editForm.company || ""}
onChange={(e) => setEditForm({ ...editForm, company: e.target.value })}
/>
</FormField>
<FormField label="Status">
<select
value={editForm.status || ""}
onChange={(e) => setEditForm({ ...editForm, status: e.target.value as any })}
>
{STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</FormField>
<div style={{ marginTop: 24 }}>
<button onClick={handleSave} disabled={saving}>
{saving ? "Saving..." : "Save"}
</button>
<button onClick={handleCancel} style={{ marginLeft: 8 }}>Cancel</button>
</div>
</div>
) : (
<div style={{ marginTop: 16 }}>
<InfoRow label="Name" value={customer.name} />
<InfoRow label="Phone" value={customer.phone} />
<InfoRow label="Company" value={customer.company || "-"} />
<InfoRow label="Status" value={getStatusLabel(customer.status)} />
<InfoRow label="Created At" value={new Date(customer.create_time).toLocaleString()} />
<div style={{ marginTop: 24 }}>
<button onClick={() => setIsEditing(true)}>Edit</button>
<button onClick={handleDelete} style={{ marginLeft: 8, color: "red" }}>
Delete
</button>
</div>
</div>
)}
</div>
);
}
// Helper components
function FormField({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div style={{ marginBottom: 16 }}>
<label style={{ display: "block", marginBottom: 4, fontWeight: "bold" }}>{label}</label>
<div>{children}</div>
</div>
);
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<p style={{ margin: "8px 0" }}>
<strong>{label}:</strong>{value}
</p>
);
}
Section Summary
You learned:
| API | Purpose | Return Value |
|---|---|---|
getOne(id) | Fetch a single record | The data object itself |
update(id, data) | Update data | Complete updated data |
delete(id) | Delete data | None (WebAPI mode only) |
FAQ
Q: What's the difference between data returned by getOne and filter?
getOne(id)- Returns the data object directly{ id, name, ... }filter()- Returns a pagination structure{ tableData, total, ... }
Q: Do I need to pass all fields when updating?
No. Only pass the fields you want to modify; other fields will remain unchanged. This also helps avoid data overwriting issues during concurrent modifications.
Q: What should I do if delete functionality fails?
Delete functionality is only available in WebAPI mode (Cookie authentication). If you're using OpenAPI mode, consider using "soft delete":
// Soft delete: Mark as deleted instead of physical deletion
await lovrabetClient.models.customers.update(id, {
status: "deleted",
deleted_at: new Date().toISOString()
});