useOptimistic
TheuseOptimistic hook provides a wrapper around React’s useOptimistic hook with built-in support for common CRUD operations (add, update, delete) on arrays of data. It’s perfect for implementing optimistic UI updates before server confirmation.
Installation
npm install @craft-ui/hooks
Import
import { useOptimistic } from "@craft-ui/hooks";
Usage
import { useOptimistic } from "@craft-ui/hooks";
interface Todo {
id: string;
text: string;
completed: boolean;
}
function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [optimisticTodos, updateOptimisticTodos] = useOptimistic(initialTodos);
const addTodo = async (text: string) => {
const newTodo = { id: crypto.randomUUID(), text, completed: false };
// Update UI immediately
updateOptimisticTodos({ action: "add", item: newTodo });
// Send to server
await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(newTodo),
});
};
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Parameters
An array of data items. Each item must have an
id property of type string.Returns
The hook returns a tuple:The optimistic data array that reflects both the actual data and any pending optimistic updates.
A function to trigger optimistic updates. Accepts an object with
action (“add” | “update” | “delete”) and item (the data item).Type Definition
interface OptimisticUpdate<TData> {
action: "add" | "delete" | "update";
item: TData;
}
function useOptimistic<TData extends { id: string }>(
data: TData[]
): readonly [
TData[],
(update: OptimisticUpdate<TData>) => void
];
Examples
Todo List with CRUD Operations
import { useOptimistic } from "@craft-ui/hooks";
import { useState } from "react";
interface Todo {
id: string;
text: string;
completed: boolean;
}
function TodoApp({ initialTodos }: { initialTodos: Todo[] }) {
const [optimisticTodos, updateOptimistic] = useOptimistic(initialTodos);
const [input, setInput] = useState("");
const addTodo = async () => {
const newTodo: Todo = {
id: crypto.randomUUID(),
text: input,
completed: false,
};
updateOptimistic({ action: "add", item: newTodo });
setInput("");
await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(newTodo),
});
};
const toggleTodo = async (todo: Todo) => {
const updated = { ...todo, completed: !todo.completed };
updateOptimistic({ action: "update", item: updated });
await fetch(`/api/todos/${todo.id}`, {
method: "PATCH",
body: JSON.stringify(updated),
});
};
const deleteTodo = async (todo: Todo) => {
updateOptimistic({ action: "delete", item: todo });
await fetch(`/api/todos/${todo.id}`, {
method: "DELETE",
});
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && addTodo()}
/>
<button onClick={addTodo}>Add</button>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo)}
/>
<span
style={{
textDecoration: todo.completed ? "line-through" : "none",
}}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Shopping Cart
import { useOptimistic } from "@craft-ui/hooks";
interface CartItem {
id: string;
productId: string;
name: string;
quantity: number;
price: number;
}
function ShoppingCart({ initialItems }: { initialItems: CartItem[] }) {
const [optimisticCart, updateCart] = useOptimistic(initialItems);
const addToCart = async (product: { id: string; name: string; price: number }) => {
const item: CartItem = {
id: crypto.randomUUID(),
productId: product.id,
name: product.name,
quantity: 1,
price: product.price,
};
updateCart({ action: "add", item });
await fetch("/api/cart", {
method: "POST",
body: JSON.stringify(item),
});
};
const updateQuantity = async (item: CartItem, quantity: number) => {
const updated = { ...item, quantity };
updateCart({ action: "update", item: updated });
await fetch(`/api/cart/${item.id}`, {
method: "PATCH",
body: JSON.stringify({ quantity }),
});
};
const removeFromCart = async (item: CartItem) => {
updateCart({ action: "delete", item });
await fetch(`/api/cart/${item.id}`, { method: "DELETE" });
};
const total = optimisticCart.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<div>
<h2>Cart ({optimisticCart.length} items)</h2>
{optimisticCart.map((item) => (
<div key={item.id}>
<span>{item.name}</span>
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item, Number(e.target.value))}
min={1}
/>
<span>${item.price * item.quantity}</span>
<button onClick={() => removeFromCart(item)}>Remove</button>
</div>
))}
<div>Total: ${total}</div>
</div>
);
}
Comment System
import { useOptimistic } from "@craft-ui/hooks";
import { useState } from "react";
interface Comment {
id: string;
author: string;
text: string;
likes: number;
createdAt: string;
}
function CommentSection({ initialComments }: { initialComments: Comment[] }) {
const [optimisticComments, updateComments] = useOptimistic(initialComments);
const [newComment, setNewComment] = useState("");
const addComment = async () => {
const comment: Comment = {
id: crypto.randomUUID(),
author: "Current User",
text: newComment,
likes: 0,
createdAt: new Date().toISOString(),
};
updateComments({ action: "add", item: comment });
setNewComment("");
await fetch("/api/comments", {
method: "POST",
body: JSON.stringify(comment),
});
};
const likeComment = async (comment: Comment) => {
const updated = { ...comment, likes: comment.likes + 1 };
updateComments({ action: "update", item: updated });
await fetch(`/api/comments/${comment.id}/like`, { method: "POST" });
};
const deleteComment = async (comment: Comment) => {
updateComments({ action: "delete", item: comment });
await fetch(`/api/comments/${comment.id}`, { method: "DELETE" });
};
return (
<div>
<div>
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Write a comment..."
/>
<button onClick={addComment}>Post Comment</button>
</div>
<div>
{optimisticComments.map((comment) => (
<div key={comment.id}>
<strong>{comment.author}</strong>
<p>{comment.text}</p>
<button onClick={() => likeComment(comment)}>
Like ({comment.likes})
</button>
<button onClick={() => deleteComment(comment)}>Delete</button>
</div>
))}
</div>
</div>
);
}
File Upload Manager
import { useOptimistic } from "@craft-ui/hooks";
interface FileItem {
id: string;
name: string;
size: number;
status: "uploading" | "completed" | "failed";
progress: number;
}
function FileUploader() {
const [optimisticFiles, updateFiles] = useOptimistic<FileItem>([]);
const uploadFile = async (file: File) => {
const fileItem: FileItem = {
id: crypto.randomUUID(),
name: file.name,
size: file.size,
status: "uploading",
progress: 0,
};
updateFiles({ action: "add", item: fileItem });
try {
// Simulate upload with progress
for (let i = 0; i <= 100; i += 10) {
await new Promise((resolve) => setTimeout(resolve, 200));
updateFiles({
action: "update",
item: { ...fileItem, progress: i },
});
}
updateFiles({
action: "update",
item: { ...fileItem, status: "completed", progress: 100 },
});
} catch (error) {
updateFiles({
action: "update",
item: { ...fileItem, status: "failed" },
});
}
};
const removeFile = async (file: FileItem) => {
updateFiles({ action: "delete", item: file });
};
return (
<div>
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) uploadFile(file);
}}
/>
<ul>
{optimisticFiles.map((file) => (
<li key={file.id}>
<span>{file.name}</span>
<span>{file.status}</span>
{file.status === "uploading" && (
<progress value={file.progress} max={100} />
)}
<button onClick={() => removeFile(file)}>Remove</button>
</li>
))}
</ul>
</div>
);
}
Common Patterns
Error Handling with Rollback
const addItem = async (item: Item) => {
// Store original state for rollback
const original = [...optimisticData];
// Optimistic update
updateOptimistic({ action: "add", item });
try {
await fetch("/api/items", {
method: "POST",
body: JSON.stringify(item),
});
} catch (error) {
// Rollback on error
// You'd need to implement this by managing state separately
console.error("Failed to add item", error);
}
};
Batch Updates
const deleteMultiple = async (items: Item[]) => {
// Optimistically remove all items
items.forEach(item => {
updateOptimistic({ action: "delete", item });
});
// Send batch delete request
await fetch("/api/items/batch-delete", {
method: "POST",
body: JSON.stringify({ ids: items.map(i => i.id) }),
});
};
Notes
- All data items must have an
idproperty of typestring - The hook uses React’s experimental
useOptimistichook under the hood - Optimistic updates are immediately reflected in the UI
- The actual data prop should be updated when the server confirms the operation
- The hook automatically handles add, update, and delete operations
- For updates, items are matched by their
idproperty - This hook requires React 18 or later with experimental features enabled
- Consider implementing error handling and rollback strategies for failed operations