13

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?

Youssouf Oumar
  • 29,373
  • 11
  • 46
  • 65
Ondřej Ševčík
  • 1,049
  • 3
  • 15
  • 31
  • You can probably use a useEffect hook function that refresh the data once an events occurs. – phemieny7 Jan 15 '23 at 10:58
  • Unfortunately, useEffect is no longer usable in Server components. I could easily solve that by using Redux and converting it to client component but that defeats the purpose of server components – Ondřej Ševčík Jan 15 '23 at 11:02

3 Answers3

20

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.

Youssouf Oumar
  • 29,373
  • 11
  • 46
  • 65
6

https://stackoverflow.com/a/75127011/17964403 This is Great for mutating on the client side but if you want to do something like search/filtering using input on the client side and want to re-fetch the same data you can do something like

const [searchterm, setSearchterm] = useState("");
const handleSearch = (e) => {
   e.preventDefault();
   if(!searchterm)return
   router.push(/home?search=`${searchterm}`)
}

and in the server component, you will receive the search param as a prop, see if the search param exists, if so then in the fetch call pass that and you will get filtered items.

ayush jain
  • 61
  • 2
0

I'd like to add something similar, which I guess someone need.

Suppose you have a login page, which is a client component. And you have a todolist page, which is a server component, to display the user's todo items.

If you switch from the login page to the todolist page, for example, you have completed the login.

Suppose you use "router.push('/todolist')" to jump, but sometimes the todolist may not refreshed, it shows the last user's todo items.

You can try this:

router.push(`/todolist?${Math.random().toString()}`);
hyyou2010
  • 791
  • 11
  • 22