Skip to content

Commit

Permalink
Deprecate fetcher.type and fetcher.submission
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Mar 7, 2023
1 parent b33d098 commit 77740fb
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/deprecate-fetcher-type-submission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/react": patch
---

Deprecate `fetcher.type` and `fetcher.submission` for Remix v2
4 changes: 2 additions & 2 deletions docs/guides/optimistic-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Remix can help you build optimistic UI with [`useNavigation`][use-navigation] an
## Strategy

1. User submits a form (or you do with [`useSubmit`][use-submit] or [`fetcher.submit`][fetcher-submission]).
2. Remix makes the submission and its data immediately available to you on [`navigation.formData`][navigation-formdata] or [`fetcher.submission`][fetcher-submission].
3. App uses [`submission.formData`][form-data] to render an optimistic version of _what it will render_ when the submission completes successfully.
2. Remix makes the submission and its data immediately available to you on [`navigation.formData`][navigation-formdata] or [`fetcher.formData`][fetcher-submission].
3. App uses [`formData`][form-data] to render an optimistic version of _what it will render_ when the submission completes successfully.
4. Remix automatically revalidates all the data.
- If successful, the user doesn't even notice.
- If it fails, the page data is automatically in sync with the server so the UI reverts automatically.
Expand Down
79 changes: 59 additions & 20 deletions docs/hooks/use-fetcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ function SomeComponent() {

// build UI with these
fetcher.state;
fetcher.type;
fetcher.submission;
fetcher.formMethod;
fetcher.formAction;
fetcher.formData;
fetcher.formEncType;
fetcher.data;
}
```
Expand All @@ -71,6 +73,8 @@ You can know the state of the fetcher with `fetcher.state`. It will be one of:

#### `fetcher.type`

<docs-error>`fetcher.type` is deprecated and will be removed in v2.</docs-error>

This is the type of state the fetcher is in. It's like `fetcher.state`, but more granular. Depending on the fetcher's state, the types can be the following:

- `state === "idle"`
Expand All @@ -89,8 +93,47 @@ This is the type of state the fetcher is in. It's like `fetcher.state`, but more
- **actionRedirect** - The action from an "actionSubmission" returned a redirect and the page is transitioning to the new location.
- **normalLoad** - A route's loader is being called without a submission (`fetcher.load()`).

##### Moving away from `fetcher.type`

The `type` field has been been deprecated and will be removed in v2. We've found that `state` is sufficient for almost all use-cases, and when it's not you can derive sub-types via `fetcher.state` and other fields. Here's a few examples:

```js
function Component() {
let fetcher = useFetcher();

let isDone =
fetcher.state === "idle" && fetcher.data != null;

let isActionSubmission = fetcher.state === "submitting";

let isActionReload =
fetcher.state === "loading" &&
fetcher.formMethod != null &&
fetcher.formMethod != "get" &&
// If we returned data, we must be reloading
fetcher.data != null;

let isActionRedirect =
fetcher.state === "loading" &&
fetcher.formMethod != null &&
navigation.formMethod != "get" &&
// If we have no data we must have redirected
fetcher.data == null;

let isLoaderSubmission =
navigation.state === "loading" &&
navigation.state.formMethod === "get";

let isNormalLoad =
navigation.state === "loading" &&
navigation.state.formMethod == null;
}
```

#### `fetcher.submission`

<docs-error>`fetcher.submission` is deprecated and will be removed in v2. Instead, the fields inside of `submission` have been flattened onto the `fetcher` itself (`fetcher.formMethod`, `fetcher.formAction`, `fetcher.formData`, `fetcher.formEncType`)</docs-error>

When using `<fetcher.Form>` or `fetcher.submit()`, the form submission is available to build optimistic UI.

It is not available when the fetcher state is "idle" or "loading".
Expand Down Expand Up @@ -153,7 +196,7 @@ function SomeComponent() {
const fetcher = useFetcher();

useEffect(() => {
if (fetcher.type === "init") {
if (fetcher.state === "idle" && fetcher.data == null) {
fetcher.load("/some/route");
}
}, [fetcher]);
Expand Down Expand Up @@ -204,7 +247,10 @@ function NewsletterSignup() {
const ref = useRef();

useEffect(() => {
if (newsletter.type === "done" && newsletter.data.ok) {
if (
newsletter.state === "idle" &&
newsletter.data?.ok
) {
ref.current.reset();
}
}, [newsletter]);
Expand All @@ -225,7 +271,7 @@ function NewsletterSignup() {
</button>
</p>

{newsletter.type === "done" ? (
{newsletter.state === "idle" && newsletter.data ? (
newsletter.data.ok ? (
<p>Thanks for subscribing!</p>
) : newsletter.data.error ? (
Expand Down Expand Up @@ -283,18 +329,12 @@ export function NewsletterSignup() {
Form={newsletter.Form}
data={newsletter.data}
state={newsletter.state}
type={newsletter.type}
/>
);
}

// used here and in the route
export function NewsletterForm({
Form,
data,
state,
type,
}) {
export function NewsletterForm({ Form, data, state }) {
// refactor a bit in here, just read from props instead of useFetcher
}
```
Expand All @@ -309,12 +349,7 @@ import { NewsletterForm } from "~/NewsletterSignup";
export default function NewsletterSignupRoute() {
const data = useActionData<typeof action>();
return (
<NewsletterForm
Form={Form}
data={data}
state="idle"
type="done"
/>
<NewsletterForm Form={Form} data={data} state="idle" />
);
}
```
Expand Down Expand Up @@ -355,7 +390,11 @@ function UserAvatar({ partialUser }) {
const [showDetails, setShowDetails] = useState(false);

useEffect(() => {
if (showDetails && userDetails.type === "init") {
if (
showDetails &&
userDetails.state === "idle" &&
!userDetails.data
) {
userDetails.load(`/users/${user.id}/details`);
}
}, [showDetails, userDetails]);
Expand All @@ -367,7 +406,7 @@ function UserAvatar({ partialUser }) {
>
<img src={partialUser.profileImageUrl} />
{showDetails ? (
userDetails.type === "done" ? (
userDetails.state === "idle" && userDetails.data ? (
<UserPopup user={userDetails.data} />
) : (
<UserPopupLoading />
Expand Down
16 changes: 8 additions & 8 deletions docs/hooks/use-fetchers.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ For example, imagine a UI where the sidebar lists projects, and the main view di
+-----------------+----------------------------┘
```

When the user clicks a checkbox, the submission goes to the action to change the state of the task. Instead of creating a "loading state" we want to create an "optimistic UI" that will **immediately** update the checkbox to appear checked even though the server hasn't processed it yet. In the checkbox component, we can use `fetcher.submission`:
When the user clicks a checkbox, the submission goes to the action to change the state of the task. Instead of creating a "loading state" we want to create an "optimistic UI" that will **immediately** update the checkbox to appear checked even though the server hasn't processed it yet. In the checkbox component, we can use `fetcher.formData`:

```tsx
function Task({ task }) {
const toggle = useFetcher();
const checked = toggle.submission
const checked = toggle.formData
? // use the optimistic version
Boolean(toggle.submission.formData.get("complete"))
Boolean(toggle.formData.get("complete"))
: // use the normal version
task.complete;

Expand Down Expand Up @@ -81,7 +81,7 @@ This is where `useFetchers` comes in. Up in the sidebar, we can access all the i
The strategy has three steps:

1. Find the submissions for tasks in a specific project
2. Use the `fetcher.submission.formData` to immediately update the count
2. Use the `fetcher.formData` to immediately update the count
3. Use the normal task's state if it's not inflight

Here's some sample code:
Expand All @@ -95,15 +95,15 @@ function ProjectTaskCount({ project }) {
const myFetchers = new Map();
for (const f of fetchers) {
if (
f.submission &&
f.submission.action.startsWith(
f.formAction &&
f.formAction.startsWith(
`/projects/${project.id}/task`
)
) {
const taskId = f.submission.formData.get("id");
const taskId = f.formData.get("id");
myFetchers.set(
parseInt(taskId),
f.submission.formData.get("complete") === "on"
f.formData.get("complete") === "on"
);
}
}
Expand Down
60 changes: 55 additions & 5 deletions packages/remix-react/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1361,17 +1361,19 @@ function convertNavigationToTransition(navigation: Navigation): Transition {
*/
export function useFetchers(): Fetcher[] {
let fetchers = useFetchersRR();
return fetchers.map((f) =>
convertRouterFetcherToRemixFetcher({
return fetchers.map((f) => {
let fetcher = convertRouterFetcherToRemixFetcher({
state: f.state,
data: f.data,
formMethod: f.formMethod,
formAction: f.formAction,
formData: f.formData,
formEncType: f.formEncType,
" _hasFetcherDoneAnything ": f[" _hasFetcherDoneAnything "],
})
);
});
addFetcherDeprecationWarnings(fetcher);
return fetcher;
});
}

export type FetcherWithComponents<TData> = Fetcher<TData> & {
Expand Down Expand Up @@ -1403,15 +1405,63 @@ export function useFetcher<TData = any>(): FetcherWithComponents<
formEncType: fetcherRR.formEncType,
" _hasFetcherDoneAnything ": fetcherRR[" _hasFetcherDoneAnything "],
});
return {
let fetcherWithComponents = {
...remixFetcher,
load: fetcherRR.load,
submit: fetcherRR.submit,
Form: fetcherRR.Form,
};
addFetcherDeprecationWarnings(fetcherWithComponents);
return fetcherWithComponents;
}, [fetcherRR]);
}

function addFetcherDeprecationWarnings(fetcher: Fetcher) {
let type: Fetcher["type"] = fetcher.type;
Object.defineProperty(fetcher, "type", {
get() {
warnOnce(
false,
"⚠️ DEPRECATED: The `useFetcher().type` field has been deprecated and " +
"will be removed in Remix v2. Please update your code to rely on " +
"`fetcher.state`.\n\nSee https://remix.run/docs/hooks/use-fetcher for " +
"more information."
);
return type;
},
set(value: Fetcher["type"]) {
// Devs should *not* be doing this but we don't want to break their
// current app if they are
type = value;
},
// These settings should make this behave like a normal object `type` field
configurable: true,
enumerable: true,
});

let submission: Fetcher["submission"] = fetcher.submission;
Object.defineProperty(fetcher, "submission", {
get() {
warnOnce(
false,
"⚠️ DEPRECATED: The `useFetcher().submission` field has been deprecated and " +
"will be removed in Remix v2. The submission fields now live directly " +
"on the fetcher (`fetcher.formData`).\n\n" +
"See https://remix.run/docs/hooks/use-fetcher for more information."
);
return submission;
},
set(value: Fetcher["submission"]) {
// Devs should *not* be doing this but we don't want to break their
// current app if they are
submission = value;
},
// These settings should make this behave like a normal object `type` field
configurable: true,
enumerable: true,
});
}

function convertRouterFetcherToRemixFetcher(
fetcherRR: Omit<ReturnType<typeof useFetcherRR>, "load" | "submit" | "Form">
): Fetcher {
Expand Down

0 comments on commit 77740fb

Please sign in to comment.