For data modifications (POST, PUT, DELETE), we need different patterns than read queries.
The Hook
jsximport { useState, useCallback } from 'react'; function useMutation(mutationFn, options = {}) { const { onSuccess, onError, onSettled } = options; const [status, setStatus] = useState('idle'); const [data, setData] = useState(null); const [error, setError] = useState(null); const mutate = useCallback( async (variables) => { setStatus('loading'); setError(null); try { const result = await mutationFn(variables); setData(result); setStatus('success'); onSuccess?.(result, variables); return result; } catch (err) { setError(err); setStatus('error'); onError?.(err, variables); throw err; } finally { onSettled?.(data, error, variables); } }, [mutationFn, onSuccess, onError, onSettled] ); const reset = useCallback(() => { setStatus('idle'); setData(null); setError(null); }, []); return { mutate, reset, status, data, error, isIdle: status === 'idle', isLoading: status === 'loading', isSuccess: status === 'success', isError: status === 'error', }; }
Usage: Create Todo
jsxconst createTodo = async (todo) => { const response = await fetch('/api/todos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(todo), }); return response.json(); }; function AddTodo({ onTodoAdded }) { const [text, setText] = useState(''); const { mutate, isLoading, isError, error, reset } = useMutation( createTodo, { onSuccess: (newTodo) => { setText(''); // Clear input onTodoAdded(newTodo); // Notify parent }, } ); const handleSubmit = (e) => { e.preventDefault(); mutate({ text, completed: false }); }; return ( <form onSubmit={handleSubmit}> <input value={text} onChange={(e) => setText(e.target.value)} placeholder="New todo" disabled={isLoading} /> <button type="submit" disabled={isLoading}> {isLoading ? 'Adding...' : 'Add'} </button> {isError && ( <div className="error"> {error.message} <button type="button" onClick={reset}>Dismiss</button> </div> )} </form> ); }
Usage: Delete Item
jsxconst deleteItem = async (id) => { await fetch(`/api/items/${id}`, { method: 'DELETE' }); return id; }; function ItemList({ items, onItemDeleted }) { const { mutate: deleteMutation, isLoading } = useMutation( deleteItem, { onSuccess: onItemDeleted } ); return ( <ul> {items.map(item => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation(item.id)} disabled={isLoading} > Delete </button> </li> ))} </ul> ); }