Skip to content

Commit

Permalink
Revert "useActionData: actions can return anything"
Browse files Browse the repository at this point in the history
This reverts commit 1a6f09f.
  • Loading branch information
ryanflorence committed Aug 12, 2021
1 parent f161d3d commit 841b5f8
Show file tree
Hide file tree
Showing 43 changed files with 305 additions and 625 deletions.
34 changes: 29 additions & 5 deletions docs/api/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export let loader = () => {
};
export default function Users() {
let data = useLoaderData();
let data = useRouteData();
return (
<ul>
{data.map(user => (
Expand Down Expand Up @@ -376,7 +376,7 @@ Now your route component can deal with it:
```tsx
export default function Something() {
let data = useLoaderData();
let data = useRouteData();
if (data.error) {
return <ErrorMessage>{data.message}</ErrorMessage>;
Expand All @@ -390,9 +390,9 @@ The initial server render will get a 500 for this page, and client side transiti
## action
Like `loader`, action is a server only function to handle data mutations and other actions. If a non-GET request is made to your route (POST, PUT, PATCH, DELETE) then the deepest matching route action is called before the loaders page.
Like `loader`, action is a server only function to handle data mutations and other actions. If a non-GET request is made to your route (POST, PUT, PATCH, DELETE) then the route's action is called instead of its loader.
Actions are triggered from `<Form method="post | put | patch | delete" />` submits.
Actions are triggered from `<form method="post">` or Remix `<Form method="post | put | patch | delete" />` submits. Note you must always return a redirect (we do this so users can't click "back" and accidentally resubmit the form).
```tsx
import { redirect } from "remix";
Expand All @@ -408,7 +408,31 @@ export let action = async ({ params, request }) => {
data: Object.fromEntries(data)
});
return redirect(`/posts/${params.postId}`);
return `/posts/${params.postId}`;
};
```
You must return a redirect of some sort, there are three, depending on your needs:
```tsx
export let action = async () => {
// you can return a string
return `/posts/${params.postId}`;
// or use the redirect helper, useful when committing sessions
return redirect(`/posts/${params.postId}`, {
headers: {
"Set-Cookie": await commitSession()
}
});
// or if you want to get really low level, construct your own response
return new Response("", {
status: 303,
headers: {
Location: "/somewhere"
}
});
};
```
Expand Down
22 changes: 11 additions & 11 deletions docs/api/constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The Remix compiler can automatically remove server code from the browser bundles
Consider a route module that exports `loader`, `meta`, and a component:

```tsx
import { useLoaderData } from "remix";
import { useRouteData } from "remix";
import PostsView from "../PostsView";
import { prisma } from "../db";

Expand All @@ -30,7 +30,7 @@ export function meta() {
}

export default function Posts() {
let posts = useLoaderData();
let posts = useRouteData();
return <PostsView posts={posts} />;
}
```
Expand All @@ -46,15 +46,15 @@ export { meta, default } from "./routes/posts.tsx";
The compiler will now analyze the code in `routes/posts.tsx` and only keep code that's inside of `meta` and the component. The result is something like this:

```tsx
import { useLoaderData } from "remix";
import { useRouteData } from "remix";
import PostsView from "../PostsView";

export function meta() {
return { title: "Posts" };
}

export default function Posts() {
let posts = useLoaderData();
let posts = useRouteData();
return <PostsView posts={posts} />;
}
```
Expand All @@ -72,7 +72,7 @@ Simply put, a **side effect** is any code that might _do something_. A **module
Taking our code from earlier, we saw how the compiler can remove the exports and their imports that aren't used. But if we add this seemingly harmless line of code your app will break!

```tsx bad lines=5
import { useLoaderData } from "remix";
import { useRouteData } from "remix";
import PostsView from "../PostsView";
import { prisma } from "../db";

Expand All @@ -87,15 +87,15 @@ export function meta() {
}

export default function Posts() {
let posts = useLoaderData();
let posts = useRouteData();
return <PostsView posts={posts} />;
}
```

That `console.log` _does something_. The module is imported and then immediately logs to the console. The compiler won't remove it because it has to run when the module is imported. It will bundle something like this:

```tsx bad lines=3,5
import { useLoaderData } from "remix";
import { useRouteData } from "remix";
import PostsView from "../PostsView";
import { prisma } from "../db"; //😬

Expand All @@ -106,7 +106,7 @@ export function meta() {
}

export default function Posts() {
let posts = useLoaderData();
let posts = useRouteData();
return <PostsView posts={posts} />;
}
```
Expand All @@ -116,7 +116,7 @@ The loader is gone but the prisma dependency stayed! Had we logged something har
To fix this, remove the side effect by simply moving the code _into the loader_.

```tsx [6]
import { useLoaderData } from "remix";
import { useRouteData } from "remix";
import PostsView from "../PostsView";
import { prisma } from "../db";

Expand All @@ -130,7 +130,7 @@ export function meta() {
}

export default function Posts() {
let posts = useLoaderData();
let posts = useRouteData();
return <PostsView posts={posts} />;
}
```
Expand Down Expand Up @@ -247,7 +247,7 @@ Maybe seeing how this would be used will help:
export let action = async ({ request }) => {
return withSession(request, session => {
session.flash("message", "Functional Composition is Fun! (ctional)");
return redirect("/this/same/page");
return "/this/same/page";
});
};

Expand Down
147 changes: 28 additions & 119 deletions docs/api/remix.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,43 @@ export default function App() {
}
```

## `useLoaderData`
## `useRouteData`

This hook returns the JSON parsed data from your route data loader.
This hook returns the data from your route data loader.

```tsx [2,9]
import React from "react";
import { useLoaderData } from "remix";
import { useRouteData } from "remix";

export function loader() {
return { some: "data" };
}

export default function Invoices() {
let invoices = useLoaderData();
let invoices = useRouteData();
// ...
}
```

`useRouteData` can be useful for creating abstractions that do some transformation on the data for your route. For example, with Firebase you could build a hook that turns the static data fetched from the server into a live document in the client.

```tsx
// if you're using firebase you could build a "live" route document
function useLiveRouteData() {
// your server could return the path of the data it fetched, and the data on
// two keys. If all routes loaders with live data follow this convention, it
// doesn't matter which route we're using this hook in
let { path, data } = useRouteData();

// maybe you've got a firebase abstraction that takes a path to subscribe to
// and initial data.
let liveData = useFirestoreDoc(path, data);

// return the live data
return liveData;
}
```

## `usePendingLocation`

During a clientside route transition, Remix loads the resources for the next page before continuing the transition (because we're all sick of flickering spinners). But we also need some UI to acknowledge that they clicked a link. This is the purpose of this hook.
Expand Down Expand Up @@ -193,116 +212,6 @@ Note: has no effect without JavaScript on the page.

If true, it will submit the form with the browser instead of JavaScript, even if JavaScript is on the page.

## `useActionData`

This hook returns the JSON parsed data from your route action. If no form has been submit, it returns `undefined`.

```tsx [2,9]
import React from "react";
import { useActionData } from "remix";

export function action() {
return { message: "hello" };
}

export default function Invoices() {
let data = useActionData();
return <div>{data ? data.message : "Waiting..."}</div>;
}
```

<docs-info>It is not expected to use this hook very often, you should redirect from actions most of the time.</docs-info>

Form submits are navigation events in browsers, which means users can click the back button into a location that had a form submit _and the browser will resubmit the form_. You usually don't even want this to happen.

For example, consider this user flow:

1. The user lands at `/buy`
2. They submit a form to `/checkout`
3. The click a link to `/order/123`

The history stack looks like this, where "\*" is the current entry:

```
GET /buy -> POST /checkout -> *GET /order/123
```

Now consider the user clicks the back button 😨

```
GET /buy -> *POST /checkout <- GET /order/123
```

The browser will repost the same information and likely charge their credit card again. You usually don't want this.

The decades-old best practice is to redirect in the POST request. This way the location disappears from the browser's history stack and the user can't "back into it" anymore.

```
GET /buy -> POST /checkout, Redirect -> GET /order/123
```

This results in a history stack that looks like this:

```
GET /buy -> *GET /order/123
```

Now the user can click back without resubmitting the form.

With progressively enhanced Forms, Remix follows the browser behavior by resubmitting forms when the user clicks back, forward, or refreshes into the location. If you don't want the form to be resubmit on back clicks/refreshes you will want to redirect out of your actions.

So, if you're supposed to redirect from actions instead of return data, you might be wondering wtheck is the point of this hook? The most common use-case is form validation errors. If the form isn't right, you can simply return the errors and let the user try again (instead of pushing all the errors into sessions).

```tsx
import { redirect, json, Form } from "remix";

export function action({ request }) {
let body = Object.fromEntries(new URLSearchParams(await request.text()));
let errors = {};

// validate the fields
if (!body.email.includes("@")) {
errors.email = "That doesn't look like an email address";
}

if (body.password.length < 6) {
errors.password = "Password must be > 6 characters";
}

// return data if we have errors
if (Object.keys(errors).length) {
return json(errors, { status: 422 });
}

// otherwise create the user and redirect
await createUser(body);
return redirect("/dashboard");
}

export default function Signup() {
let errors = useActionData();

return (
<>
<h1>Signup</h1>
<Form method="post">
<p>
<input type="text" name="email" />
{errors?.email && <span>{errors.email}</span>}
</p>
<p>
<input type="text" name="password" />
{errors?.password && <span>{errors.password}</span>}
</p>
<p>
<button type="submit">Sign up</button>
</p>
</Form>
</>
);
}
```

## `useFormAction`

Resolves the value of a `<form action>` attribute using React Router's relative paths. This can be useful when computing the correct action for a `<button formAction>`, for example, when a `<button>` changes the action of its `<form>`.
Expand Down Expand Up @@ -478,7 +387,7 @@ You can put whatever you want on a route `handle`, here we'll use `breadcrumb`,

```tsx [5, 16-22]
// root.tsx
import { Links, Scripts, useLoaderData, useMatches } from "remix";
import { Links, Scripts, useRouteData, useMatches } from "remix";

export default function Root() {
let matches = useMatches();
Expand Down Expand Up @@ -644,7 +553,7 @@ Then, you can `import` the cookie and use it in your `loader` and/or `action`. T
```js
// app/routes/index.js
import React from "react";
import { useLoaderData, json, redirect } from "remix";
import { useRouteData, json, redirect } from "remix";

import { userPrefs as cookie } from "../cookies";

Expand All @@ -670,7 +579,7 @@ export async function action({ request }) {
}

export default function Home() {
let { showBanner } = useLoaderData();
let { showBanner } = useRouteData();

return (
<div>
Expand Down Expand Up @@ -947,7 +856,7 @@ export async function action({ request }) {
}
export default function Login() {
let { currentUser, error } = useLoaderData();
let { currentUser, error } = useRouteData();
return (
<div>
Expand Down Expand Up @@ -1189,7 +1098,7 @@ export async function loader({ request }) {
}

export default function App() {
let { message } = useLoaderData();
let { message } = useRouteData();

return (
<html>
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/envvars.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Instead we recommmend keeping all of your environment variables on the server (a
}

export function Root() {
let data = useLoaderData();
let data = useRouteData();
return (
<html lang="en">
<head>
Expand Down
Loading

0 comments on commit 841b5f8

Please sign in to comment.