Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

revalidatePath / single Server request with updated rsc data #85

Closed
6 tasks done
aheissenberger opened this issue Dec 4, 2024 · 6 comments
Closed
6 tasks done
Assignees
Labels
bug Something isn't working

Comments

@aheissenberger
Copy link
Contributor

aheissenberger commented Dec 4, 2024

Describe the bug

When using Next.js it is possible to call a server action and update the interface with one network request.
The request to the server will contain the payload and the response will return the result of the server function and the update serialized data of the components.

The interface in Next.js ist updated based on the use of revalidatePath('/') in the server action addTodo().

How can I achieve this with the react-server framework? I tried to use refresh() which should reload the navigation including data from the server and leads with Next.js and other frameworks to an extra request to the server to update the interface. The goal should be to remove refresh() and only use revalidatePath('/') in the server action.

'use client'

import { useState } from 'react'
//import { useRouter } from 'next/navigation'
import { useClient } from "@lazarv/react-server/client";
import { addTodo } from '../../lib/actions'

export function AddTodoForm() {
  const [text, setText] = useState('')
  //const router = useRouter()
  const { refresh } = useClient();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (text.trim()) {
      await addTodo(text)
      setText('')
      //router.refresh()
      refresh()
    }
  }

  return (
    <form onSubmit={handleSubmit} className="mb-4">
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo"
        className="border p-2 mr-2"
      />
      <button type="submit" className="bg-blue-500 text-white p-2 rounded">
        Add Todo
      </button>
    </form>
  )
}
'use server'

import { revalidatePath } from 'next/cache'
import { addTodoToStore, toggleTodoInStore, deleteTodoFromStore } from './todos'

export async function addTodo(text: string) {
  await addTodoToStore(text)
  revalidatePath('/')
}

Next.js 15 todo app:
https://github.com/aheissenberger/demo-nextjs-todo

Request Payload by the Next.js 15 framework:
SCR-20241204-svfr
Request Response:
SCR-20241204-svja

Reproduction

https://github.com/aheissenberger/demo-react-server-todo

Steps to reproduce

No response

System Info

System:
    OS: macOS 15.1.1
    CPU: (12) arm64 Apple M4 Pro
    Memory: 1.08 GB / 48.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 23.3.0 - /opt/homebrew/bin/node
    Yarn: 1.22.22 - /opt/homebrew/bin/yarn
    npm: 10.9.0 - /opt/homebrew/bin/npm
    pnpm: 9.14.4 - /opt/homebrew/bin/pnpm
    bun: 1.1.38 - /opt/homebrew/bin/bun
  Browsers:
    Chrome: 131.0.6778.108
    Safari: 18.1.1
    Safari Technology Preview: 18.2
  npmPackages:
    @lazarv/react-server: 0.0.0-experimental-61cbefd-20241204-b7fff15a => 0.0.0-experimental-61cbefd-20241204-b7fff15a

Used Package Manager

pnpm

Logs

No response

Validations

@aheissenberger aheissenberger added the pending triage Pending triage label Dec 4, 2024
@lazarv
Copy link
Owner

lazarv commented Dec 5, 2024

Hi! Sadly, this is not possible right now in the same way as in Next.js.

When calling a server function from a client component by a function call, the request will expect a JSON response from the server. While using a server function as a form action, it is actually a page (or outlet) refresh using an RSC payload response. So, the workaround is to use a form action instead of importing the server function in the client component and just calling it. You can render the arguments of the server function into a hidden input element to send the trimmed value. Another option is to use a client component to render the new item and you can also include useOptimistic, which can't work with server components.

revalidatePath in Next.js is doing more with this behaviour as it's not only invalidating the cache, but also affecting what the response will be. While in react-server revalidate is just invalidating the HTTP response cache. The essential difference is in how caching was approached by the two frameworks.

I think that react-server provides a more traditional approach. The function call is like an API call, just returning the data sent by the server function and using a server function as a form action is refreshing the page like with plain HTML forms.

Where react-server could improve is in supporting more server function return types, like binary data and even other types of transports, like WebSocket or SSE.

@aheissenberger
Copy link
Contributor Author

But why will refresh() from useClient() not reload the RSC data? This should initiate a second request and update the UI.

@aheissenberger
Copy link
Contributor Author

I think that react-server provides a more traditional approach. The function call is like an API call, just returning the data sent by the server function and using a server function as a form action is refreshing the page like with plain HTML forms.

Where react-server could improve is in supporting more server function return types, like binary data and even other types of transports, like WebSocket or SSE.

Have a look at the Waku RSC framework as they have implemented a solution to this problem:
dai-shi/waku#1033

Providing single request mutation call with UI updates is an important feature of a RSC framework as this gives it a big performance advantage compared to other data loading solutions.

When look at the server function from Next.js, they allow to return any standard response object.
The result of `revalidatePath('/mypath') should be similar to a request by the client for the RSC data and the result is a response object. The harder part is on the client side to detect the RSC data stream and feed this stream back into the react tree.

@lazarv
Copy link
Owner

lazarv commented Dec 6, 2024

I've found your issue with using refresh() in your reproduction repo with react-server. Right now there's a bug when you don't return any JSON.stringify-able value from a server function when you call the server function from a client component. So the server function in your case is breaking the execution flow and refresh is never actually called. The workaround is to return null; at the end of each server function.

Refresh, navigation, and any client-side routing should work as expected, and using any RSC payload to inject into a ReactServerComponent is ready. That's how outlets and RSC Delegation work.

You can also implement single roundtrip mutations by using form actions instead of directly calling the server function and when the request payload is a FormData, it will also refresh the parent outlet with the new RSC payload rendered by the server after executing the server function.

What's missing is the capability to simultaneously return any arbitrary value from the server function and re-render the page/outlet and the ability to give the intent to request this from the framework. I think that what Next.js is doing is that it injects the return value of the server function into the RSC payload and then it handles the combined RSC payload on the client. Nothing is impossible here and also it would be easy to add a framework API to give this intent in the server function, similar to how redirect or cache invalidation works.

I'll fix the issue with handling a server function return value when it's a Promise<void> against this issue and create a new enhancement issue for the follow-up work regarding the new framework API.

@lazarv lazarv self-assigned this Dec 6, 2024
@lazarv lazarv added bug Something isn't working and removed pending triage Pending triage labels Dec 6, 2024
lazarv added a commit that referenced this issue Dec 8, 2024
Server functions now return result in RSC payload. This helps with
supporting multiple types of results (including binary formats) and also
enables the new `reload()` API which enables single-roundtrip mutation
and page (or outlet) refresh by combining the server function result
with the rendered component in a single RSC payload.

Includes tests for multiple server function result types and
documentation update to include `reload()`.

This PR addresses an issue with server functions without a return value
(`Promise<void>`) and enhances the framework to be able to return a
rendered component along with the server function return value discussed
in #85.
@lazarv
Copy link
Owner

lazarv commented Dec 8, 2024

I'll fix the issue with handling a server function return value when it's a Promise against this issue and create a new enhancement issue for the follow-up work regarding the new framework API.

@aheissenberger as the linked PR describes it, I ended up using an RSC payload for all server functions to support as many return types as possible (including React components), and this also enabled implementing reload(). I added a very minimal part about this new API in the docs at https://react-server.dev/router/server-routing#reload.

Copy link

This issue has been locked since it has been closed for more than 30 days.

If you have found a concrete bug or regression related to it, please open a new bug report with a reproduction against the latest version of @lazarv/react-server. If you have any other comments you should create a new discussion.

@github-actions github-actions bot locked and limited conversation to collaborators Jan 18, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants