Next.js Updating Data Cheat Sheet

Client-Side Mutations

Basic Form Submission

'use client';

import { useState } from 'react';

export default function ContactForm() {
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        message: ''
    });
    const [isSubmitting, setIsSubmitting] = useState(false);
    const [error, setError] = useState(null);

    const handleSubmit = async (e) => {
        e.preventDefault();
        setIsSubmitting(true);
        setError(null);

        try {
            const response = await fetch('/api/contact', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(formData),
            });

            if (!response.ok) {
                throw new Error('Failed to submit form');
            }

            const result = await response.json();
            console.log('Success:', result);
            
            // Reset form
            setFormData({ name: '', email: '', message: '' });
        } catch (err) {
            setError(err.message);
        } finally {
            setIsSubmitting(false);
        }
    };

    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(prev => ({
            ...prev,
            [name]: value
        }));
    };

    return (
        <form onSubmit={handleSubmit}>
            {error && <div className="error">{error}</div>}
            
            <input
                type="text"
                name="name"
                value={formData.name}
                onChange={handleChange}
                placeholder="Your name"
                required
            />
            
            <input
                type="email"
                name="email"
                value={formData.email}
                onChange={handleChange}
                placeholder="Your email"
                required
            />
            
            <textarea
                name="message"
                value={formData.message}
                onChange={handleChange}
                placeholder="Your message"
                required
            />
            
            <button type="submit" disabled={isSubmitting}>
                {isSubmitting ? 'Sending...' : 'Send Message'}
            </button>
        </form>
    );
}

Optimistic Updates

'use client';

import { useState } from 'react';

export default function TodoList({ initialTodos }) {
    const [todos, setTodos] = useState(initialTodos);

    const addTodo = async (text) => {
        const newTodo = {
            id: Date.now(),
            text,
            completed: false
        };

        // Optimistic update
        setTodos(prev => [...prev, newTodo]);

        try {
            const response = await fetch('/api/todos', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ text }),
            });

            if (!response.ok) {
                throw new Error('Failed to add todo');
            }

            const savedTodo = await response.json();
            
            // Update with server response
            setTodos(prev => 
                prev.map(todo => 
                    todo.id === newTodo.id ? savedTodo : todo
                )
            );
        } catch (error) {
            // Revert optimistic update
            setTodos(prev => prev.filter(todo => todo.id !== newTodo.id));
            console.error('Error adding todo:', error);
        }
    };

    const toggleTodo = async (id) => {
        const todo = todos.find(t => t.id === id);
        
        // Optimistic update
        setTodos(prev => 
            prev.map(t => 
                t.id === id ? { ...t, completed: !t.completed } : t
            )
        );

        try {
            const response = await fetch(`/api/todos/${id}`, {
                method: 'PATCH',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ completed: !todo.completed }),
            });

            if (!response.ok) {
                throw new Error('Failed to update todo');
            }
        } catch (error) {
            // Revert optimistic update
            setTodos(prev => 
                prev.map(t => 
                    t.id === id ? { ...t, completed: todo.completed } : t
                )
            );
            console.error('Error updating todo:', error);
        }
    };

    return (
        <div>
            <form onSubmit={(e) => {
                e.preventDefault();
                const text = e.target.todo.value;
                if (text.trim()) {
                    addTodo(text);
                    e.target.todo.value = '';
                }
            }}>
                <input name="todo" placeholder="Add new todo" />
                <button type="submit">Add</button>
            </form>

            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        <input
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => toggleTodo(todo.id)}
                        />
                        <span style={{ 
                            textDecoration: todo.completed ? 'line-through' : 'none' 
                        }}>
                            {todo.text}
                        </span>
                    </li>
                ))}
            </ul>
        </div>
    );
}

Server Actions

Basic Server Action

// app/actions/todoActions.js
'use server';

import { revalidatePath } from 'next/cache';

export async function addTodo(formData) {
    const text = formData.get('text');
    
    if (!text || text.trim().length === 0) {
        return { error: 'Todo text is required' };
    }

    try {
        // Add to database
        const todo = await db.todo.create({
            data: { text: text.trim() }
        });

        // Revalidate the todos page
        revalidatePath('/todos');
        
        return { success: true, todo };
    } catch (error) {
        return { error: 'Failed to add todo' };
    }
}

export async function updateTodo(id, completed) {
    try {
        await db.todo.update({
            where: { id },
            data: { completed }
        });

        revalidatePath('/todos');
        return { success: true };
    } catch (error) {
        return { error: 'Failed to update todo' };
    }
}

export async function deleteTodo(id) {
    try {
        await db.todo.delete({
            where: { id }
        });

        revalidatePath('/todos');
        return { success: true };
    } catch (error) {
        return { error: 'Failed to delete todo' };
    }
}

Using Server Actions in Components

// app/todos/page.js
import { addTodo, updateTodo, deleteTodo } from '@/app/actions/todoActions';

export default function TodosPage() {
    return (
        <div>
            <form action={addTodo}>
                <input 
                    name="text" 
                    placeholder="Add new todo" 
                    required 
                />
                <button type="submit">Add Todo</button>
            </form>

            <TodoList />
        </div>
    );
}

// app/components/TodoList.jsx
'use client';

import { useTransition } from 'react';
import { updateTodo, deleteTodo } from '@/app/actions/todoActions';

export default function TodoList({ todos }) {
    const [isPending, startTransition] = useTransition();

    const handleToggle = (id, completed) => {
        startTransition(() => {
            updateTodo(id, !completed);
        });
    };

    const handleDelete = (id) => {
        startTransition(() => {
            deleteTodo(id);
        });
    };

    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>
                    <input
                        type="checkbox"
                        checked={todo.completed}
                        onChange={() => handleToggle(todo.id, todo.completed)}
                        disabled={isPending}
                    />
                    <span style={{ 
                        textDecoration: todo.completed ? 'line-through' : 'none' 
                    }}>
                        {todo.text}
                    </span>
                    <button 
                        onClick={() => handleDelete(todo.id)}
                        disabled={isPending}
                    >
                        Delete
                    </button>
                </li>
            ))}
            {isPending && <div>Updating...</div>}
        </ul>
    );
}

API Routes for Data Updates

CRUD API Routes

// app/api/todos/route.js
import { NextResponse } from 'next/server';
import { revalidatePath } from 'next/cache';

export async function GET() {
    try {
        const todos = await db.todo.findMany({
            orderBy: { createdAt: 'desc' }
        });
        
        return NextResponse.json(todos);
    } catch (error) {
        return NextResponse.json(
            { error: 'Failed to fetch todos' },
            { status: 500 }
        );
    }
}

export async function POST(request) {
    try {
        const { text } = await request.json();
        
        if (!text || text.trim().length === 0) {
            return NextResponse.json(
                { error: 'Todo text is required' },
                { status: 400 }
            );
        }

        const todo = await db.todo.create({
            data: { text: text.trim() }
        });

        revalidatePath('/todos');
        
        return NextResponse.json(todo, { status: 201 });
    } catch (error) {
        return NextResponse.json(
            { error: 'Failed to create todo' },
            { status: 500 }
        );
    }
}

Dynamic API Routes

// app/api/todos/[id]/route.js
import { NextResponse } from 'next/server';
import { revalidatePath } from 'next/cache';

export async function GET(request, { params }) {
    try {
        const todo = await db.todo.findUnique({
            where: { id: params.id }
        });

        if (!todo) {
            return NextResponse.json(
                { error: 'Todo not found' },
                { status: 404 }
            );
        }

        return NextResponse.json(todo);
    } catch (error) {
        return NextResponse.json(
            { error: 'Failed to fetch todo' },
            { status: 500 }
        );
    }
}

export async function PATCH(request, { params }) {
    try {
        const { text, completed } = await request.json();
        
        const todo = await db.todo.update({
            where: { id: params.id },
            data: { 
                ...(text !== undefined && { text }),
                ...(completed !== undefined && { completed })
            }
        });

        revalidatePath('/todos');
        revalidatePath(`/todos/${params.id}`);
        
        return NextResponse.json(todo);
    } catch (error) {
        return NextResponse.json(
            { error: 'Failed to update todo' },
            { status: 500 }
        );
    }
}

export async function DELETE(request, { params }) {
    try {
        await db.todo.delete({
            where: { id: params.id }
        });

        revalidatePath('/todos');
        
        return NextResponse.json({ success: true });
    } catch (error) {
        return NextResponse.json(
            { error: 'Failed to delete todo' },
            { status: 500 }
        );
    }
}

Caching and Revalidation

Cache Invalidation

// lib/cache.js
import { revalidatePath, revalidateTag } from 'next/cache';

export async function invalidateTodos() {
    // Invalidate specific paths
    revalidatePath('/todos');
    revalidatePath('/dashboard');
    
    // Invalidate by tag
    revalidateTag('todos');
}

// app/api/todos/route.js
export async function POST(request) {
    try {
        const { text } = await request.json();
        
        const todo = await db.todo.create({
            data: { text }
        });

        // Invalidate cache
        await invalidateTodos();
        
        return NextResponse.json(todo);
    } catch (error) {
        return NextResponse.json(
            { error: 'Failed to create todo' },
            { status: 500 }
        );
    }
}

Conditional Revalidation

// app/api/posts/route.js
export async function POST(request) {
    try {
        const { title, content, category } = await request.json();
        
        const post = await db.post.create({
            data: { title, content, category }
        });

        // Revalidate based on category
        revalidatePath('/posts');
        revalidatePath(`/category/${category}`);
        
        // Revalidate home page if it's a featured post
        if (post.featured) {
            revalidatePath('/');
        }
        
        return NextResponse.json(post);
    } catch (error) {
        return NextResponse.json(
            { error: 'Failed to create post' },
            { status: 500 }
        );
    }
}

Real-time Updates

WebSocket Integration

'use client';

import { useEffect, useState } from 'react';

export default function RealTimeTodos() {
    const [todos, setTodos] = useState([]);
    const [ws, setWs] = useState(null);

    useEffect(() => {
        const socket = new WebSocket('ws://localhost:3001');
        
        socket.onopen = () => {
            console.log('Connected to WebSocket');
        };
        
        socket.onmessage = (event) => {
            const data = JSON.parse(event.data);
            
            if (data.type === 'TODO_ADDED') {
                setTodos(prev => [...prev, data.todo]);
            } else if (data.type === 'TODO_UPDATED') {
                setTodos(prev => 
                    prev.map(todo => 
                        todo.id === data.todo.id ? data.todo : todo
                    )
                );
            } else if (data.type === 'TODO_DELETED') {
                setTodos(prev => 
                    prev.filter(todo => todo.id !== data.todoId)
                );
            }
        };
        
        socket.onclose = () => {
            console.log('Disconnected from WebSocket');
        };
        
        setWs(socket);
        
        return () => {
            socket.close();
        };
    }, []);

    const addTodo = async (text) => {
        const response = await fetch('/api/todos', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ text })
        });
        
        if (response.ok) {
            // WebSocket will handle the update
            console.log('Todo added, waiting for real-time update');
        }
    };

    return (
        <div>
            <form onSubmit={(e) => {
                e.preventDefault();
                const text = e.target.todo.value;
                if (text.trim()) {
                    addTodo(text);
                    e.target.todo.value = '';
                }
            }}>
                <input name="todo" placeholder="Add new todo" />
                <button type="submit">Add</button>
            </form>

            <ul>
                {todos.map(todo => (
                    <li key={todo.id}>
                        <input
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => toggleTodo(todo.id)}
                        />
                        <span>{todo.text}</span>
                    </li>
                ))}
            </ul>
        </div>
    );
}

Error Handling and Validation

Form Validation

'use client';

import { useState } from 'react';

export default function ValidatedForm() {
    const [formData, setFormData] = useState({
        email: '',
        password: '',
        confirmPassword: ''
    });
    const [errors, setErrors] = useState({});
    const [isSubmitting, setIsSubmitting] = useState(false);

    const validateForm = () => {
        const newErrors = {};

        // Email validation
        if (!formData.email) {
            newErrors.email = 'Email is required';
        } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
            newErrors.email = 'Email is invalid';
        }

        // Password validation
        if (!formData.password) {
            newErrors.password = 'Password is required';
        } else if (formData.password.length < 8) {
            newErrors.password = 'Password must be at least 8 characters';
        }

        // Confirm password validation
        if (formData.password !== formData.confirmPassword) {
            newErrors.confirmPassword = 'Passwords do not match';
        }

        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
    };

    const handleSubmit = async (e) => {
        e.preventDefault();
        
        if (!validateForm()) {
            return;
        }

        setIsSubmitting(true);

        try {
            const response = await fetch('/api/register', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(formData)
            });

            if (!response.ok) {
                const errorData = await response.json();
                setErrors(errorData.errors || { general: 'Registration failed' });
                return;
            }

            // Success
            console.log('Registration successful');
        } catch (error) {
            setErrors({ general: 'Network error' });
        } finally {
            setIsSubmitting(false);
        }
    };

    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(prev => ({
            ...prev,
            [name]: value
        }));
        
        // Clear error when user starts typing
        if (errors[name]) {
            setErrors(prev => ({
                ...prev,
                [name]: ''
            }));
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            {errors.general && (
                <div className="error">{errors.general}</div>
            )}

            <div>
                <input
                    type="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                    placeholder="Email"
                    className={errors.email ? 'error' : ''}
                />
                {errors.email && <span className="error-text">{errors.email}</span>}
            </div>

            <div>
                <input
                    type="password"
                    name="password"
                    value={formData.password}
                    onChange={handleChange}
                    placeholder="Password"
                    className={errors.password ? 'error' : ''}
                />
                {errors.password && <span className="error-text">{errors.password}</span>}
            </div>

            <div>
                <input
                    type="password"
                    name="confirmPassword"
                    value={formData.confirmPassword}
                    onChange={handleChange}
                    placeholder="Confirm Password"
                    className={errors.confirmPassword ? 'error' : ''}
                />
                {errors.confirmPassword && (
                    <span className="error-text">{errors.confirmPassword}</span>
                )}
            </div>

            <button type="submit" disabled={isSubmitting}>
                {isSubmitting ? 'Registering...' : 'Register'}
            </button>
        </form>
    );
}

Best Practices

Debounced Updates

'use client';

import { useState, useEffect, useCallback } from 'react';

export default function DebouncedInput({ initialValue, onSave }) {
    const [value, setValue] = useState(initialValue);
    const [debouncedValue, setDebouncedValue] = useState(initialValue);

    // Debounce effect
    useEffect(() => {
        const timer = setTimeout(() => {
            setDebouncedValue(value);
        }, 500);

        return () => clearTimeout(timer);
    }, [value]);

    // Save when debounced value changes
    useEffect(() => {
        if (debouncedValue !== initialValue) {
            onSave(debouncedValue);
        }
    }, [debouncedValue, onSave, initialValue]);

    return (
        <input
            type="text"
            value={value}
            onChange={(e) => setValue(e.target.value)}
            placeholder="Type to search..."
        />
    );
}

Batch Updates

'use client';

import { useState, useCallback } from 'react';

export default function BatchUpdater() {
    const [updates, setUpdates] = useState([]);
    const [isProcessing, setIsProcessing] = useState(false);

    const addUpdate = useCallback((update) => {
        setUpdates(prev => [...prev, update]);
    }, []);

    const processBatch = async () => {
        if (updates.length === 0) return;

        setIsProcessing(true);
        
        try {
            const response = await fetch('/api/batch-update', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ updates })
            });

            if (response.ok) {
                setUpdates([]); // Clear processed updates
                console.log('Batch update successful');
            }
        } catch (error) {
            console.error('Batch update failed:', error);
        } finally {
            setIsProcessing(false);
        }
    };

    return (
        <div>
            <button onClick={() => addUpdate({ type: 'increment', id: 1 })}>
                Add Update
            </button>
            
            <button 
                onClick={processBatch}
                disabled={updates.length === 0 || isProcessing}
            >
                {isProcessing ? 'Processing...' : `Process ${updates.length} Updates`}
            </button>

            <div>
                Pending updates: {updates.length}
            </div>
        </div>
    );
}