Mutating Data

The Next.js team is working on a new RFC for mutating data in Next.js. This RFC has not been published yet. For now, we recommend the following pattern:

After a data mutation, you can use router.refresh() to refresh (fetch updated data and re-render on the server) the current route from the root layout down.

Example

Let's consider a list view. Inside your Server Component, you fetch the list of items:

app/page.tsx
import Todo from './todo';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

async function getTodos() {
  const res = await fetch('https://api.example.com/todos');
  const todos: Todo[] = await res.json();
  return todos;
}

export default async function Page() {
  const todos = await getTodos();
  return (
    <ul>
      {todos.map((todo) => (
        <Todo key={todo.id} {...todo} />
      ))}
    </ul>
  );
}

Each item has its own Client Component. This allows the component to use event handlers (like onClick or onSubmit) to trigger a mutation.

app/todo.tsx
"use client";

import { useRouter } from 'next/navigation';
import { useState, useTransition } from 'react';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

export default function Todo(todo: Todo) {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();
  const [isFetching, setIsFetching] = useState(false);

  // Create inline loading UI
  const isMutating = isFetching || isPending;

  async function handleChange() {
    setIsFetching(true);
    // Mutate external data source
    await fetch(`https://api.example.com/todo/${todo.id}`, {
      method: 'PUT',
      body: JSON.stringify({ completed: !todo.completed }),
    });
    setIsFetching(false);

    startTransition(() => {
      // Refresh the current route and fetch new data from the server without
      // losing client-side browser or React state.
      router.refresh();
    });
  }

  return (
    <li style={{ opacity: !isMutating ? 1 : 0.7 }}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={handleChange}
        disabled={isPending}
      />
      {todo.title}
    </li>
  );
}

By calling router.refresh(), the current route will refresh and fetch an updated list of todos from the server. This does not affect browser history, but it does refresh data from the root layout down. When using refresh(), client-side state is not lost, including both React and browser state.

We can use the useTransition hook to create inline loading UI. We wrap the mutation in a startTransition function to mark the update as a transition and use the isPending flag to show loading UI while the transition is pending.