Shopify / hydrogen-v1

React-based framework for building dynamic, Shopify-powered custom storefronts.
https://shopify.github.io/hydrogen-v1/
MIT License
3.75k stars 327 forks source link

Support mutations & refreshing RSC responses from API endpoints #883

Open jplhomer opened 2 years ago

jplhomer commented 2 years ago

What happens if someone makes a request to an API Route, and the result of the API route needs to re-trigger RSC?

Here's how the React RSC demo handles it: https://github.com/reactjs/server-components-demo/blob/main/src/NoteEditor.client.js

  1. Call to API endpoint, including the "intended location" aka the server state requested for the next page load as a URL param
  2. API endpoint executes (performing mutation, etc)
  3. API endpoint calls renderToReadableStream using the provided server state param
  4. Client code receives this as part of useMutation and passes it to useRefresh() along with the key of the serverState which should be updated (in this case, the current page pathname + search). We already have a useRefresh function, but we need to update it to support clearing a given cache value manually.

We should push on this and support rendering bespoke RSC endpoints from an API function using a nice affordance.

One result of this could be a new useMutation hook, just like the RSC demo has in the NoteEditor file.

useMutation(
  endpoint: string,
  method?: HTTPMethod, // default to POST
  redirectTo?: string, // default to current location (aka serverState)
);

// usage:

const [updatingCart, updateCart] = useMutation('/cart');

async function handleCartUpdate() {
  await updateCart({ lines: newLines });
};

// cart.server.js

export async function api(request, {renderServerComponentsResponse}) {
  // do cart stuff

  // This helper function is seeded with the `redirectTo` URL that `useMutation` injected as a private param.
  // You could always override it, too, e.g. if there was a failure and you wanted to return someone elsewhere.
  // It is injected in entry-server.tsx
  return renderServerComponentsResponse();
}

Where useMutation abstracts a number of responsibilities:

  1. Calling the API endpoint with the redirectTo intended server state
  2. Taking the response and seeding it in the correct key with useRefresh
  3. Calling useNavigate() to navigate to that new key/serverState.

Originally posted by @jplhomer in https://github.com/Shopify/hydrogen/issues/881#issuecomment-1064423802

blittle commented 2 years ago

I love this proposal! A few thoughts:

  1. I assume the server state would include a combination of: locationServerState, persistedServerState, & userServerState
  2. What if like remix, we recommend (or provide an option to) use a <Form> component for mutations? That form
// default to a method="post", and the action would also default to the current url
<Form action="/cart" method="post" >
  <input type="hidden" name="productId" value={productId} />
  <button type="submit">Add to cart</button>
</Form>

And the <Form> component would automatically add a few other hidden form fields:

  <input type="hidden" name="redirect" value="/somePath" />
  <input type="hidden" name="serverState" value="stringified server state" />

So when you click the submit button

  1. fetch is still used, but a FormData object is sent.
  2. In your API route handler, you use the native FormData object to .get('productId') and do the graphql request to add it to a cart
  3. API route returns renderServerComponentsResponse(), and that uses the server state that was previously sent up to re-RSC and return the RSC response. Also new server state is sent down by a header (this is what happens in the official server components demo but I'm not sure all the situations why this would change? Maybe explicitly changed in the call to renderServerComponentsResponse?)
  4. The client rehydrates with the server response.

The interesting thing here is even with JS disabled, those values would all still get sent in the form submit request. Maybe a separate hidden input field could tell the server that JS is disabled. Then the server would do it's normal things, but on the call to renderServerComponentsResponse it would instead just send a 301 redirect to the redirect path (or the same location). So the browser would refresh completely when you click add to cart, but it would still work.

jplhomer commented 2 years ago

Sounds dope @blittle!!

I was also thinking that maybe we don't even need a renderServerComponentsResponse in user code by default. Hydrogen knows it's coming from a <Form> or useMutation call, so it can call that automatically. The user can always explicitly call it if they want to redirect to a different place that the original intent.