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

Loaders for mutations with <Form/> #33

Closed
ryanflorence opened this issue Nov 3, 2020 · 2 comments
Closed

Loaders for mutations with <Form/> #33

ryanflorence opened this issue Nov 3, 2020 · 2 comments
Assignees
Labels
enhancement New feature or request

Comments

@ryanflorence
Copy link
Member

ryanflorence commented Nov 3, 2020

Today, to do a mutation, you typically need to create a server api route (express, or otherwise) and wire it all up with a bunch of JavaScript (that most of us do a poor job of cobbling together).

We've had multiple people already trying to use loaders as mutation endpoints, and we ourselves would like to as well.

A strong philosophy in Remix is that you should be able to build websites the "web 1.0 way". When it comes to mutations in HTML, the name of the game is <form method=post>. The developer experience with HTML forms for mutations is actually really great.

Web 1.0 Mutations

  1. Make a form
<form method="post" action="/projects/create">
  <input type="text" name="title" />
  <input type="text" name="description" />
  <button type="submit">Create Project</button>
</form>
  1. Handle it on the server
app.post("/projects/create", async (req, res) => {
  let project = await createProject(req.body);
  res.redirect(`/projects/${project.id}`);
});

The end. It was so nice. No screwing around serializing the form, no loading states, or useEffect, or dealing with asynchronous APIs like fetch. Just make a form, the browser serializes the form, handles the asynchrony, and the server redirects to the new page. It's even a built-in state machine where the URLs are the states, which drastically simplifies the code you write.

Why we do it client side

If you go to https://remix.run/newsletter you can see our submit button animates between states and just feels really fun. Stripe checkout has wonderful loading/error states as well.

Additionally, many projects have "wizard" like flows to create things, moving through routes makes a lot of sense but you can't really animate that, or there's state in the UI that you don't want to lose in a browser navigation.

This kind of stuff is typically not possible with web 1.0 style posts.

Why not both?

Remix is positioned to allow for both the superior developer experience of writing mutations as forms (the way HTML and HTTP are designed) while also enabling great user experiences like stripe checkout.

Currently, you can post to a Remix loader if you set up your server that way. For example, in express you can do app.all("*", createRequestHandler()). However, it pretty much only works if you're submit plain HTML forms w/o client side navigation.

With a new <Form/> component, we can add support for mutations on route loaders without doing full page reloads so that you can preserve state on the page for improved UX.

This means error handling, pending states, etc. are all normal transition states of Remix, but now for form mutations.

Big example:

The component:

import * as React from "react";
import { useRouteData, Form } from "@remix-run/react";

export default function NewProject() {
  let data = useRouteData();

  return (
    <>
      <h1>New Project</h1>
      <Form to="/projects/new" method="post">
        <p>
          <label>
            Name:
            <br />
            <input name="name" defaultValue={data?.body.name} />
          </label>
          {data?.errors.name && <Error>{data.errors.name}</Error>}
        </p>
        <p>
          <label>
            Description:
            <br />
            <textarea
              cols={20}
              name="description"
              defaultValue={data?.body.description}
            />
          </label>
          {data?.errors.description && (
            <Error>{data?.errors.description}</Error>
          )}
        </p>
        <p>
          <button type="submit">Create</button>
        </p>
      </Form>
    </>
  );
}

The loader:

import { redirect } from "@remix-run/loader";
import { createProject } from "../models/project";

export default function async Loader({ context: { req }}) {
  let [project, errors] = await createProject(body);

  if (project) {
    return redirect(`/projects/${project.id}`);
  } else if (errors) {
    return { errors, body };
  }
};

Remix Form does a fetch(..., { method: "post" }) to the loader, the loader creates a record and redirects, or it returns the errors and body to the component--which happens to be the very same route thats already rendering. Because this is all React, these are all just state changes inside of the components already rendered.

That means you can use isPending to do loading effects on the button, or even animate in the errors.

But the developer experience is identical to plain HTML forms! In fact, you could even disable JavaScript and it this would all still work. I guess that's what they meant by "progressive enhancement" 🤪

Form is like Link

React Router can't really ship with a <Form> component because it doesn't know about a server. Since Remix is a server, we can complete the browser navigation picture with client side routing. <Link> for get, <Form> for mutations 🎉

Implementation Notes

  • <Form> could navigate(to, { state: { method, body }}) to trigger @remix-run/react.fetchData to use the proper method.
    • could instead make a remix specific navigate like navigate(to, { method, body }) or navigate.post(to, body), just some ideas.
  • If we want to support put and delete, can use a <input type="hidden" name="__method" value="put"> since HTML forms don't support it.

Might be a few other things, but in general I think that's all we need.

Next level: turn loaders into http controllers

If we wanted to take this a step further, each deployment wrapper (express, aws, etc.) could add a body parser, and do method branching on the loader, so loaders could end up having an interface of exporting get, post, put, ad delete methods:

exports.get = ({ params, context, url }) => {}
exports.post = ({ params, body, context, url }) => {}
exports.put = ({ params, body, context, url }) => {}
exports.delete = ({ params, context, url }) => {}

Can wait on this controller stuff though, all we need right now is <Form> and navigate(to, { state: { method, body }})

@ryanflorence ryanflorence added the enhancement New feature or request label Nov 3, 2020
@ryanflorence ryanflorence self-assigned this Nov 4, 2020
@sambs
Copy link

sambs commented Nov 18, 2020

Seams neat.

I'm wondering about what happens with more complicated mutations involving arrays or nested data structures. I guess this could be acheived using some kind of extended urlencoded syntax such as parsed by the qs module.

Would the navigate support sending json too?

@MichaelDeBoey
Copy link
Member

We have actions, so I think this is covered?

@machour machour closed this as completed Mar 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants