It's time to get serious. This step in the tutorial is one of the things that makes Remix really unique so we hope you'll take the time to follow along completely.
We're going to be creating data here. If you've been doing web development for a decade or more, this is going to feel very familiar. If you're a little newer, you're going to be blown away by how easy it used to be, and how easy it is again with Remix. In summary we will:
<Form>
to post with JavaScriptuseTransition
The big takeaway here is that actions (and data mutations) in Remix are modeled as html form navigation. When submiting with JavaScript, Remix can make it faster and ensure the data updates appear on the entire page without a full page reload. Or, you can leave JavaScript at the door and use basic forms.
This next step won't work without one. Go to your GitHub Tokens page and create a new token. Make sure to check the box "Gists". That's all you'll need.
Go make a file at app/routes/gists.new.tsx
and put this in it.
import React from "react";
export default function NewGist() {
return (
<>
<h2>New Gist!</h2>
<form method="post">
<p>
<label>
Gist file name:
<br />
<input required type="text" name="fileName" />
</label>
</p>
<p>
<label>
Content:
<br />
<textarea required rows={10} name="content" />
</label>
</p>
<p>
<button type="submit">Create Gist</button>
</p>
</form>
</>
);
}
Notice we're using a plain HTML <form/>
with an action that points to our /gists
route, and then we name the inputs. When you submit this form, the browser will post to the gists data module--no JavaScript required. We're going to make this work with a plain <form>
first, and then we'll upgrade it to a Remix <Form>
to show how to progressively enhance the form when you have the budget to create a really nice UX with JavaScript.
Now let's go create an "action" to handle this form.
You've seen a loader
already. Now you're going to create an action
. Go back to your new route and this:
import React from "react";
import type { ActionFunction } from "remix";
export let action: ActionFunction = async ({ request }) => {
// ...
};
export default function NewGist() {
// ...
}
Quick conceptual sidebar here. Think about useState
, what does it return?
let [state, setState] = useState(initialState);
It returns the value in state and a function to change it--a "reader" and a "writer". You can think about a Remix loader as state
and a Remix action as setState
. A reader and a writer.
Now think about useReducer
:
let [state, dispatch] = useReducer(reducer, initialState);
Again we see a pair of values, one to read the state and another to update it. But this time we have a reducer
to actually handle the state update and a function to request a change to state, dispatch
.
Another way to think about Remix actions is that the reducer
is your action
and dispatch
is your <form>
. And the way we communicate an intent to change server state is with <form>
(our dispatch) HTML 1.0 style and then the action
actually deals with it (reducer
).
// reader
export let loader = () => {};
// writer
export let action = () => {};
// intent to change state
<form method="post" />;
Alright, back to our component, let's handle the form post and create a new gist with the GitHub API:
import React from "react";
import type { ActionFunction } from "remix";
import { redirect } from "remix";
let action: ActionFunction = async ({ request }) => {
// Very important or else it won't work :)
let token = "insert your github token here";
// in a real world scenario you'd want this token to be
// an enviornment variable on your server, but as long
// as you only use it in this action, it won't get
// included in the browser bundle.
// get the form body out of the request using standard web
// APIs on the server
let body = new URLSearchParams(await request.text());
// pull off what we need from the form, note they are
// named the same thing as the `<input/>` in the form.
let fileName = body.get("fileName");
let content = body.get("content");
// Hit the GitHub API to create a gist
await fetch("https://api.github.com/gists", {
method: "post",
body: JSON.stringify({
description: "Created from Remix Form!",
public: true,
files: { [fileName]: { content } }
}),
headers: {
"Content-Type": "application/json",
Authorization: `token ${token}`
}
});
// redirect out of here to go see our new gist!
return redirect("/gists");
};
export default function NewGist() {
// ... same as before
}
Alright, fill out your form and give it a shot! You should see your new gist on the /gists
page!
<Form>
and pending UI statesWith a regular <form>
we're letting the browser handle the post and the pending UI (the address bar/favicon animation). Remix has a <Form>
component and hook to go along with it to let you progressively enhance your forms. If your budget for this feature is short, just use a <form>
(or rather, <Form reloadDocument>
is the preferred way), let the browser handle it, and move on with your life.
If you've got the time to make a fancy user experience, with Remix you don't have to rewrite your code to do the fetch with useEffect
and manage your own state: you can just add the fancy bits with <Form>
.
Let's update the code and add some loading indication. Note the new imports and the capital "F" <Form>
. Now Remix is going to handle the form submit clientside with fetch
and you get access to the serialized form data in useTransition()
to build that fancy UI.
import React from "react";
import type { ActionFunction } from "remix";
import { redirect } from "remix";
import { Form, useTransition } from "remix";
export let action: ActionFunction = async ({ request }) => {
// ... same as before
};
export default function NewGist() {
let transition = useTransition();
return (
<>
<h2>New Gist!</h2>
{transition.state === "submitting" ? (
<div>
<p>
<Loading /> Creating gist:{" "}
{transition.submission.formData.get("fileName")}
</p>
</div>
) : (
/* Note the capital Form, not form */
<Form method="post">
<p>
<label>
Gist file name:
<br />
<input required type="text" name="fileName" />
</label>
</p>
<p>
<label>
Content:
<br />
<textarea required rows={10} name="content" />
</label>
</p>
<p>
<button type="submit">Create Gist</button>
</p>
</Form>
)}
</>
);
}
function Loading() {
return (
<svg
className="spin"
style={{ height: "1rem" }}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
);
}
To get the loading spinner to actually spin, put this in your css somewhere:
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
.spin {
animation: spin 1s infinite;
}
That's it! As you can see, actions + <Form>
are really powerful. They don't require JavaScript but they also enable you to build great loading expriences at the same time.