Search code examples
javascriptnode.jsreactjsmongodbnext.js

Update Server Component after data has been changed by Client Component in Next.js


I am still trying to wrap my head around this scenario. Can anyone please suggest what is the correct way to do this in Next.js 13?

I diplay a list of users in a Server Component, for example, like this (using MongoDB):

// UsersList.jsx
const UsersList = () => {
  const users = await usersCollection.getUsers()

  return (
  <div>
    {users.map(user) => <div>{user}</div>}
  </div>
  )
}

And on the same page, I have also defined client component for adding users:

// UsersEdit.jsx
'use client'
const UsersEdit = () => {
  const handleAdd() => // calls POST to /api/users
  return // render input + button
}

Both are displayed together like this in a Server Component Page:

// page.jsx
const Users = () => {
  return (
  <div>
    <UsersList />
    <UsersEdit />
  </div>
  )
}

How should I "reload" or "notify" UsersList that a new user has been added to the collection to force it to display a new user/updated user?


Solution

  • The only way to update a Server Component is to reload the page. As it's sent to the browser as static HTML without any JavaScript attached to it to have interactivity.

    To reload the page while keeping client side states, you could 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.