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

Deprecate fetcher.type and fetcher.submission #5691

Merged
merged 1 commit into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only warn if they actually access type or submission


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