To have the updated data by a Client Component reflected on a Server Component, you can use router.refresh()
, where router
is the returned value by useRouter()
. Here is an example working with a Todo List application:
Let's consider a list view. Inside your Server Component, you fetch the list of items:
// app/page.tsx
import Todo from "./todo";
async function getTodos() {
const res = await fetch("https://api.example.com/todos", { cache: 'no-store' });
const todos = 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';
export default function 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>
);
}
⚠️: refresh()
could re-produce the same result if fetch requests are cached. This is why that cache: 'no-store'
on this example.