From c3fa60bb42f9f154e2e7b65575731bd78645167d Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 18 Jul 2023 11:19:55 -0400 Subject: [PATCH] v2: Remove useTransition --- .changeset/v2-remove-use-transition.md | 9 + docs/api/remix.md | 25 - docs/guides/data-writes.md | 2 +- docs/hooks/use-fetcher.md | 6 +- docs/hooks/use-transition.md | 155 ----- integration/cf-compiler-test.ts | 1 - integration/compiler-test.ts | 1 - integration/defer-test.ts | 1 + integration/link-test.ts | 6 +- ...te-v2-test.ts => navigation-state-test.ts} | 3 - integration/transition-state-test.ts | 533 ------------------ .../app/routes/posts/admin/new.tsx | 9 +- .../utils/export.ts | 1 - .../rules/packageExports.js | 1 - packages/remix-react/components.tsx | 188 +----- packages/remix-react/index.tsx | 1 - packages/remix-react/transition.ts | 68 +-- rollup.utils.js | 1 - 18 files changed, 24 insertions(+), 987 deletions(-) create mode 100644 .changeset/v2-remove-use-transition.md delete mode 100644 docs/hooks/use-transition.md rename integration/{navigation-state-v2-test.ts => navigation-state-test.ts} (98%) delete mode 100644 integration/transition-state-test.ts diff --git a/.changeset/v2-remove-use-transition.md b/.changeset/v2-remove-use-transition.md new file mode 100644 index 00000000000..e483054b24a --- /dev/null +++ b/.changeset/v2-remove-use-transition.md @@ -0,0 +1,9 @@ +--- +"@remix-run/react": major +--- + +Remove deprecated `useTransition` hook in favor of `useNavigation`. `useNavigation` is _almost_ identical with a few exceptions:_ + +* `useTransition.type` has been removed since it can be derived from other available information +* "Submission" fields have been flattened from `useTransition().submission` down onto the root `useNavigation()` object +* `
` is now more accurately categorized as `state:"loading"` instead of `state:"submitting"` to better align with the underlying GET navigation diff --git a/docs/api/remix.md b/docs/api/remix.md index c4eb41b4b57..aad42b3799e 100644 --- a/docs/api/remix.md +++ b/docs/api/remix.md @@ -76,26 +76,6 @@ title: Remix Packages [Moved →][moved-14] -### `useTransition` - -[Moved →][moved-15] - -#### `transition.state` - -[Moved →][moved-16] - -#### `transition.type` - -[Moved →][moved-17] - -#### `transition.submission` - -[Moved →][moved-18] - -#### `transition.location` - -[Moved →][moved-19] - ### `useFetcher` [Moved →][moved-20] @@ -309,11 +289,6 @@ title: Remix Packages [moved-12]: ../hooks/use-action-data#notes-about-resubmissions [moved-13]: ../hooks/use-form-action [moved-14]: ../hooks/use-submit -[moved-15]: ../hooks/use-transition -[moved-16]: ../hooks/use-transition#transitionstate -[moved-17]: ../hooks/use-transition#transitiontype -[moved-18]: ../hooks/use-transition#transitionsubmission -[moved-19]: ../hooks/use-transition#transitionlocation [moved-20]: ../hooks/use-fetcher [moved-21]: ../hooks/use-fetcher#fetcherstate [moved-22]: ../hooks/use-fetcher#fetchertype diff --git a/docs/guides/data-writes.md b/docs/guides/data-writes.md index 7a70a62debf..1187ab9e1a6 100644 --- a/docs/guides/data-writes.md +++ b/docs/guides/data-writes.md @@ -389,7 +389,7 @@ export default function NewProject() { Pretty slick! Now when the user clicks "Create", the inputs go disabled, and the submit button's text changes. The whole operation should be faster now too since there's just one network request happening instead of a full page reload (which involves potentially more network requests, reading assets from the browser cache, parsing JavaScript, parsing CSS, etc.). -We didn't do much with `navigation` on this page, but it's got all the information about the submission, including all of the values being processed on the server in `navigation.formData`. +We didn't do much with `navigation` on this page, but it's got all the information about the submission (`navigation.formMethod`, `navigation.formAction`, `navigation.formEncType`), as well as all of the values being processed on the server on `navigation.formData`. ### Animating in the Validation Errors diff --git a/docs/hooks/use-fetcher.md b/docs/hooks/use-fetcher.md index 663be86d0bc..0a2142ab8c9 100644 --- a/docs/hooks/use-fetcher.md +++ b/docs/hooks/use-fetcher.md @@ -89,7 +89,7 @@ This is the type of state the fetcher is in. It's like `fetcher.state`, but more - `state === "loading"` - **actionReload** - The action from an "actionSubmission" returned data and the loaders on the page are being reloaded. - - **actionRedirect** - The action from an "actionSubmission" returned a redirect and the page is transitioning to the new location. + - **actionRedirect** - The action from an "actionSubmission" returned a redirect and the page is navigating to the new location. - **normalLoad** - A route's loader is being called without a submission (`fetcher.load()`). ## `fetcher.submission` @@ -185,7 +185,7 @@ See also: **Newsletter Signup Form** -Perhaps you have a persistent newsletter signup at the bottom of every page on your site. This is not a navigation event, so useFetcher is perfect for the job. First, you create a Resource Route: +Perhaps you have a persistent newsletter signup at the bottom of every page on your site. This is not a navigation event, so `useFetcher` is perfect for the job. First, you create a Resource Route: ```tsx filename=routes/newsletter/subscribe.tsx export async function action({ request }: ActionArgs) { @@ -276,7 +276,7 @@ export default function NewsletterSignupRoute() { ``` - When JS is on the page, the user will subscribe to the newsletter and the page won't change, they'll just get a solid, dynamic experience. -- When JS is not on the page, they'll be transitioned to the signup page by the browser. +- When JS is not on the page, they'll be navigated to the signup page by the browser. You could even refactor the component to take props from the hooks and reuse it: diff --git a/docs/hooks/use-transition.md b/docs/hooks/use-transition.md deleted file mode 100644 index e0cf944b44a..00000000000 --- a/docs/hooks/use-transition.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -title: useTransition ---- - -# `useTransition` - -`useTransition` will be removed in v2 in favor of [`useNavigation`][use-navigation]. You can prepare for this change at your convenience by updating to [`useNavigation`][use-navigation]. For instructions on making this change see the [v2 guide][v2guide] - -Watch the 📼 Remix Singles: Pending UI, Clearing Inputs After Form Submissions, and Optimistic UI - -This hook tells you everything you need to know about a page transition to build pending navigation indicators and optimistic UI on data mutations. Things like: - -- Global loading spinners -- Spinners on clicked links -- Disabling forms while the mutation is happening -- Adding spinners to submit buttons -- Optimistically showing a new record while it's being created on the server -- Optimistically showing the new state of a record while it's being updated - -```tsx -import { useTransition } from "@remix-run/react"; - -function SomeComponent() { - const transition = useTransition(); - transition.state; - transition.type; - transition.submission; - transition.location; -} -``` - -## `transition.state` - -You can know the state of the transition with `transition.state`. It will be one of: - -- **idle** - There is no transition pending. -- **submitting** - A form has been submitted. If GET, then the route loader is being called. If POST, PUT, PATCH, DELETE, then the route action is being called. -- **loading** - The loaders for the next routes are being called to render the next page. - -Normal navigation's transition as follows: - -``` -idle → loading → idle -``` - -GET form submissions transition as follows: - -``` -idle → submitting → idle -``` - -Form submissions with POST, PUT, PATCH, or DELETE transition as follows: - -``` -idle → submitting → loading → idle -``` - -```tsx -function SubmitButton() { - const transition = useTransition(); - - const text = - transition.state === "submitting" - ? "Saving..." - : transition.state === "loading" - ? "Saved!" - : "Go"; - - return ; -} -``` - -## `transition.type` - -Most pending UI only cares about `transition.state`, but the transition can tell you even more information on `transition.type`. - -Remix calls your route loaders at various times, like on normal link clicks or after a form submission completes. If you'd like to build pending indication that is more granular than "loading" and "submitting", use the `transition.type`. - -Depending on the transition state, the types can be the following: - -- `state === "idle"` - - - **idle** - The type is always idle when there's not a pending navigation. - -- `state === "submitting"` - - - **actionSubmission** - A form has been submitted with POST, PUT, PATCH, or DELETE, and the action is being called - - **loaderSubmission** - A form has been submitted with GET and the loader is being called - -- `state === "loading"` - - - **loaderSubmissionRedirect** - A "loaderSubmission" was redirected by the loader and the next routes are being loaded - - **actionRedirect** - An "actionSubmission" was redirected by the action and the next routes are being loaded - - **actionReload** - The action from an "actionSubmission" returned data and the loaders on the page are being reloaded - - **fetchActionRedirect** - An action [fetcher][usefetcher] redirected and the next routes are being loaded - - **normalRedirect** - A loader from a normal navigation (or redirect) redirected to a new location and the new routes are being loaded - - **normalLoad** - A normal load from a normal navigation - -```tsx -function SubmitButton() { - const transition = useTransition(); - - const loadTexts = { - actionRedirect: "Data saved, redirecting...", - actionReload: "Data saved, reloading fresh data...", - }; - - const text = - transition.state === "submitting" - ? "Saving..." - : transition.state === "loading" - ? loadTexts[transition.type] || "Loading..." - : "Go"; - - return ; -} -``` - -## `transition.submission` - -Any transition that started from a `` or `useSubmit` will have your form's submission attached to it. This is primarily useful to build "Optimistic UI" with the `submission.formData` [`FormData`][form-data] object. - -## `transition.location` - -This tells you what the next location is going to be. It's most useful when matching against the next URL for custom links and hooks. - -For example, this `Link` knows when its page is loading and about to become active: - -```tsx lines=[7-9] -import { Link, useResolvedPath } from "@remix-run/react"; - -function PendingLink({ to, children }) { - const transition = useTransition(); - const path = useResolvedPath(to); - - const isPending = - transition.state === "loading" && - transition.location.pathname === path.pathname; - - return ( - - ); -} -``` - -Note that this link will not appear "pending" if a form is being submitted to the URL the link points to, because we only do this for "loading" states. The form will contain the pending UI for when the state is "submitting", once the action is complete, then the link will go pending. - -[usefetcher]: ./use-fetcher -[form-data]: https://developer.mozilla.org/en-US/docs/Web/API/FormData -[use-navigation]: ./use-navigation -[v2guide]: ../pages/v2#usetransition diff --git a/integration/cf-compiler-test.ts b/integration/cf-compiler-test.ts index dd43597f99e..2c1edcae324 100644 --- a/integration/cf-compiler-test.ts +++ b/integration/cf-compiler-test.ts @@ -221,7 +221,6 @@ test.describe("cloudflare compiler", () => { "useResolvedPath", "useSearchParams", "useSubmit", - "useTransition", ]; let magicRemix = await fs.readFile( path.resolve(projectDir, "node_modules/remix/dist/index.js"), diff --git a/integration/compiler-test.ts b/integration/compiler-test.ts index fceb67ce1b6..61efd56bd10 100644 --- a/integration/compiler-test.ts +++ b/integration/compiler-test.ts @@ -357,7 +357,6 @@ test.describe("compiler", () => { "useResolvedPath", "useSearchParams", "useSubmit", - "useTransition", ]; let magicRemix = await fse.readFile( path.resolve(fixture.projectDir, "node_modules/remix/dist/index.js"), diff --git a/integration/defer-test.ts b/integration/defer-test.ts index 605fdedc6f8..aa49e5da735 100644 --- a/integration/defer-test.ts +++ b/integration/defer-test.ts @@ -601,6 +601,7 @@ test.describe("non-aborted", () => { test("works with critical JSON like data", async ({ page }) => { let response = await fixture.requestDocument("/"); let html = await response.text(); + console.log(html); let criticalHTML = html.slice(0, html.indexOf("") + 7); expect(criticalHTML).toContain(ROOT_ID); expect(criticalHTML).toContain(INDEX_ID); diff --git a/integration/link-test.ts b/integration/link-test.ts index d3922493ce7..2f279cafdfb 100644 --- a/integration/link-test.ts +++ b/integration/link-test.ts @@ -485,11 +485,11 @@ test.describe("route module link export", () => { { "data-test-id": "red" }, ]; } - + export default function Component() { return
; } - + export function ErrorBoundary() { return

Error Boundary

; } @@ -507,7 +507,7 @@ test.describe("route module link export", () => { { "data-test-id": "blue" }, ]; } - + export default function Component() { return
; } diff --git a/integration/navigation-state-v2-test.ts b/integration/navigation-state-test.ts similarity index 98% rename from integration/navigation-state-v2-test.ts rename to integration/navigation-state-test.ts index ff68d048efb..9bf19ff603b 100644 --- a/integration/navigation-state-v2-test.ts +++ b/integration/navigation-state-test.ts @@ -18,9 +18,6 @@ const IDLE_STATE = { state: "idle", }; -// These are a copy of the tests from navigation-state-test to test with -// future.v2_normalizeFormMethod enabled. Once we're in v2, we can delete -// the other file and keep this one. test.describe("navigation states", () => { let fixture: Fixture; let appFixture: AppFixture; diff --git a/integration/transition-state-test.ts b/integration/transition-state-test.ts deleted file mode 100644 index 55e13b5d703..00000000000 --- a/integration/transition-state-test.ts +++ /dev/null @@ -1,533 +0,0 @@ -import { test, expect } from "@playwright/test"; - -import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; -import type { Fixture, AppFixture } from "./helpers/create-fixture"; -import { PlaywrightFixture } from "./helpers/playwright-fixture"; - -const STATES = { - NORMAL_LOAD: "normal-load", - LOADING_REDIRECT: "loading-redirect", - SUBMITTING_LOADER: "submitting-loader", - SUBMITTING_LOADER_REDIRECT: "submitting-loader-redirect", - SUBMITTING_ACTION: "submitting-action", - SUBMITTING_ACTION_REDIRECT: "submitting-action-redirect", - FETCHER_REDIRECT: "fetcher-redirect", -} as const; - -const IDLE_STATE = { - state: "idle", - type: "idle", -}; - -test.describe("rendering", () => { - let fixture: Fixture; - let appFixture: AppFixture; - - test.beforeEach(async ({ context }) => { - await context.route(/_data/, async (route) => { - await new Promise((resolve) => setTimeout(resolve, 50)); - route.continue(); - }); - }); - - test.beforeAll(async () => { - fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, - files: { - "app/root.jsx": js` - import { useMemo, useRef } from "react"; - import { Outlet, Scripts, useTransition } from "@remix-run/react"; - export default function() { - const transition = useTransition(); - - const transitionsRef = useRef(); - const transitions = useMemo(() => { - const savedTransitions = transitionsRef.current || []; - savedTransitions.push(transition); - transitionsRef.current = savedTransitions; - return savedTransitions; - }, [transition]); - - return ( - - Test - - - {transition.state != "idle" && ( -

Loading...

- )} -

- - {JSON.stringify(transitions, null, 2)} - -

- - - - ); - } - `, - - "app/routes/_index.jsx": js` - import { Form, Link, useFetcher } from "@remix-run/react"; - export function loader() { return null; } - export default function() { - const fetcher = useFetcher(); - return ( -
    -
  • - - ${STATES.NORMAL_LOAD} - -
  • -
  • - - ${STATES.LOADING_REDIRECT} - -
  • -
  • - - -
  • - -
  • -
    - -
    -
  • -
  • -
    - -
    -
  • -
  • -
    - -
    -
  • -
  • - - - -
  • -
- ); - } - `, - - [`app/routes/${STATES.NORMAL_LOAD}.jsx`]: js` - export default function() { - return ( -

- ${STATES.NORMAL_LOAD} -

- ); - } - `, - - [`app/routes/${STATES.LOADING_REDIRECT}.jsx`]: js` - import { redirect } from "@remix-run/node"; - export function loader() { - return redirect("/?redirected"); - } - export default function() { - return ( -

- ${STATES.LOADING_REDIRECT} -

- ); - } - `, - - [`app/routes/${STATES.SUBMITTING_LOADER}.jsx`]: js` - export default function() { - return ( -

- ${STATES.SUBMITTING_LOADER} -

- ); - } - `, - - [`app/routes/${STATES.SUBMITTING_LOADER_REDIRECT}.jsx`]: js` - import { redirect } from "@remix-run/node"; - export function loader() { - return redirect("/?redirected"); - } - export default function() { - return ( -

- ${STATES.SUBMITTING_LOADER_REDIRECT} -

- ); - } - `, - - [`app/routes/${STATES.SUBMITTING_ACTION}.jsx`]: js` - export function loader() { return null; } - export function action() { return null; } - export default function() { - return ( -

- ${STATES.SUBMITTING_ACTION} -

- ); - } - `, - - [`app/routes/${STATES.SUBMITTING_ACTION_REDIRECT}.jsx`]: js` - import { redirect } from "@remix-run/node"; - export function action() { - return redirect("/?redirected"); - } - export default function() { - return ( -

- ${STATES.SUBMITTING_ACTION_REDIRECT} -

- ); - } - `, - - [`app/routes/${STATES.FETCHER_REDIRECT}.jsx`]: js` - import { redirect } from "@remix-run/node"; - export function action() { - return redirect("/?redirected"); - } - `, - }, - }); - - appFixture = await createAppFixture(fixture); - }); - - test.afterAll(() => { - appFixture.close(); - }); - - test("transitions to normal load (Loading)", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/", true); - await app.clickLink(`/${STATES.NORMAL_LOAD}`); - await page.waitForSelector(`#${STATES.NORMAL_LOAD}`); - await page.waitForSelector("#loading-indicator", { state: "hidden" }); - - let transitionsCode = await app.getElement("#transitions"); - let transitionsJson = transitionsCode.text(); - let transitions = JSON.parse(transitionsJson); - expect(transitions).toEqual([ - IDLE_STATE, - { - state: "loading", - type: "normalLoad", - location: { - pathname: `/${STATES.NORMAL_LOAD}`, - search: "", - hash: "", - state: null, - key: expect.any(String), - }, - }, - IDLE_STATE, - ]); - }); - - test("transitions to normal redirect (LoadingRedirect)", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/", true); - await app.clickLink(`/${STATES.LOADING_REDIRECT}`); - await page.waitForURL(/\?redirected/); - await page.waitForSelector("#loading-indicator", { state: "hidden" }); - let transitionsCode = await app.getElement("#transitions"); - let transitionsJson = transitionsCode.text(); - let transitions = JSON.parse(transitionsJson); - expect(transitions).toEqual([ - IDLE_STATE, - { - state: "loading", - type: "normalLoad", - location: { - pathname: `/${STATES.LOADING_REDIRECT}`, - search: "", - hash: "", - state: null, - key: expect.any(String), - }, - }, - { - state: "loading", - type: "normalRedirect", - location: { - pathname: "/", - search: "?redirected", - hash: "", - state: { - _isRedirect: true, - // These were private API for transition manager that are no longer - // needed with the new router so OK to disappear - // setCookie: false, - // type: "loader", - }, - key: expect.any(String), - }, - }, - IDLE_STATE, - ]); - }); - - test("transitions to loader submission (SubmittingLoader)", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/", true); - await app.clickSubmitButton(`/${STATES.SUBMITTING_LOADER}`); - await page.waitForSelector(`#${STATES.SUBMITTING_LOADER}`); - await page.waitForSelector("#loading-indicator", { state: "hidden" }); - let transitionsCode = await app.getElement("#transitions"); - let transitionsJson = transitionsCode.text(); - let transitions = JSON.parse(transitionsJson); - expect(transitions).toEqual([ - IDLE_STATE, - { - state: "submitting", - type: "loaderSubmission", - location: { - pathname: `/${STATES.SUBMITTING_LOADER}`, - search: "?key=value", - hash: "", - state: null, - key: expect.any(String), - }, - submission: { - // Note: This is a bug in Remix but we're going to keep it that way - // in useTransition (including the back-compat version) and it'll be - // fixed with useNavigation - action: `/${STATES.SUBMITTING_LOADER}?key=value`, - encType: "application/x-www-form-urlencoded", - method: "GET", - key: expect.any(String), - formData: expect.any(Object), - }, - }, - IDLE_STATE, - ]); - }); - - test("transitions to loader submission redirect (LoadingLoaderSubmissionRedirect)", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/", true); - await app.clickSubmitButton(`/${STATES.SUBMITTING_LOADER_REDIRECT}`); - await page.waitForURL(/\?redirected/); - await page.waitForSelector("#loading-indicator", { state: "hidden" }); - let transitionsCode = await app.getElement("#transitions"); - let transitionsJson = transitionsCode.text(); - let transitions = JSON.parse(transitionsJson); - expect(transitions).toEqual([ - IDLE_STATE, - { - state: "submitting", - type: "loaderSubmission", - location: { - pathname: `/${STATES.SUBMITTING_LOADER_REDIRECT}`, - search: "", - hash: "", - state: null, - key: expect.any(String), - }, - submission: { - action: `/${STATES.SUBMITTING_LOADER_REDIRECT}`, - encType: "application/x-www-form-urlencoded", - method: "GET", - key: expect.any(String), - formData: expect.any(Object), - }, - }, - { - state: "loading", - type: "loaderSubmissionRedirect", - location: { - pathname: "/", - search: "?redirected", - hash: "", - state: { - _isRedirect: true, - // These were private API for transition manager that are no longer - // needed with the new router so OK to disappear - // setCookie: false, - // type: "loader", - }, - key: expect.any(String), - }, - submission: { - action: `/${STATES.SUBMITTING_LOADER_REDIRECT}`, - encType: "application/x-www-form-urlencoded", - method: "GET", - key: expect.any(String), - formData: expect.any(Object), - }, - }, - IDLE_STATE, - ]); - }); - - test("transitions to action submission (SubmittingAction)", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/", true); - await app.clickSubmitButton(`/${STATES.SUBMITTING_ACTION}`); - await page.waitForSelector(`#${STATES.SUBMITTING_ACTION}`); - await page.waitForSelector("#loading-indicator", { state: "hidden" }); - let transitionsCode = await app.getElement("#transitions"); - let transitionsJson = transitionsCode.text(); - let transitions = JSON.parse(transitionsJson); - expect(transitions).toEqual([ - IDLE_STATE, - { - state: "submitting", - type: "actionSubmission", - location: { - pathname: `/${STATES.SUBMITTING_ACTION}`, - search: "", - hash: "", - state: null, - key: expect.any(String), - }, - submission: { - action: `/${STATES.SUBMITTING_ACTION}`, - encType: "application/x-www-form-urlencoded", - method: "POST", - key: expect.any(String), - formData: expect.any(Object), - }, - }, - { - state: "loading", - type: "actionReload", - location: { - pathname: `/${STATES.SUBMITTING_ACTION}`, - search: "", - hash: "", - state: null, - key: expect.any(String), - }, - submission: { - action: `/${STATES.SUBMITTING_ACTION}`, - encType: "application/x-www-form-urlencoded", - method: "POST", - key: expect.any(String), - formData: expect.any(Object), - }, - }, - IDLE_STATE, - ]); - }); - - test("transitions to action submission redirect (LoadingActionRedirect)", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/", true); - await app.clickSubmitButton(`/${STATES.SUBMITTING_ACTION_REDIRECT}`); - await page.waitForURL(/\?redirected/); - await page.waitForSelector("#loading-indicator", { state: "hidden" }); - let transitionsCode = await app.getElement("#transitions"); - let transitionsJson = transitionsCode.text(); - let transitions = JSON.parse(transitionsJson); - expect(transitions).toEqual([ - IDLE_STATE, - { - state: "submitting", - type: "actionSubmission", - location: { - pathname: `/${STATES.SUBMITTING_ACTION_REDIRECT}`, - search: "", - hash: "", - state: null, - key: expect.any(String), - }, - submission: { - action: `/${STATES.SUBMITTING_ACTION_REDIRECT}`, - encType: "application/x-www-form-urlencoded", - method: "POST", - key: expect.any(String), - formData: expect.any(Object), - }, - }, - { - state: "loading", - type: "actionRedirect", - location: { - pathname: "/", - search: "?redirected", - hash: "", - state: { - _isRedirect: true, - // These were private API for transition manager that are no longer - // needed with the new router so OK to disappear - // setCookie: false, - // type: "loader", - }, - key: expect.any(String), - }, - submission: { - action: `/${STATES.SUBMITTING_ACTION_REDIRECT}`, - encType: "application/x-www-form-urlencoded", - method: "POST", - key: expect.any(String), - formData: expect.any(Object), - }, - }, - IDLE_STATE, - ]); - }); - - test("transitions to fetcher action submission redirect (LoadingFetchActionRedirect)", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/", true); - await app.clickSubmitButton(`/${STATES.FETCHER_REDIRECT}`); - await page.waitForURL(/\?redirected/); - await page.waitForSelector("#loading-indicator", { state: "hidden" }); - let transitionsCode = await app.getElement("#transitions"); - let transitionsJson = transitionsCode.text(); - let transitions = JSON.parse(transitionsJson); - expect(transitions).toEqual([ - IDLE_STATE, - { - state: "loading", - type: "fetchActionRedirect", - location: { - pathname: "/", - search: "?redirected", - hash: "", - state: { - _isRedirect: true, - _isFetchActionRedirect: true, - // These were private API for transition manager that are no longer - // needed with the new router so OK to disappear - // setCookie: false, - // type: "loader", - }, - key: expect.any(String), - }, - }, - IDLE_STATE, - ]); - }); -}); diff --git a/packages/remix-dev/__tests__/fixtures/replace-remix-magic-imports/app/routes/posts/admin/new.tsx b/packages/remix-dev/__tests__/fixtures/replace-remix-magic-imports/app/routes/posts/admin/new.tsx index 1b4d9f072a5..19ec4e353d2 100644 --- a/packages/remix-dev/__tests__/fixtures/replace-remix-magic-imports/app/routes/posts/admin/new.tsx +++ b/packages/remix-dev/__tests__/fixtures/replace-remix-magic-imports/app/routes/posts/admin/new.tsx @@ -1,5 +1,6 @@ -import type { ActionFunction } from "remix"; -import { Form, redirect, json, useActionData, useTransition } from "remix"; +import type { ActionFunction } from "@remix-run/node"; +import { redirect, json } from "@remix-run/node"; +import { Form, useActionData, useNavigation } from "@remix-run/react"; import invariant from "tiny-invariant"; import { createPost } from "~/models/post.server"; @@ -37,8 +38,8 @@ const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg` export default function NewPost() { const errors = useActionData(); - const transition = useTransition(); - const isCreating = Boolean(transition.submission); + const navigation = useNavigation(); + const isCreating = Boolean(navigation.formData); return (
diff --git a/packages/remix-dev/codemod/replace-remix-magic-imports/utils/export.ts b/packages/remix-dev/codemod/replace-remix-magic-imports/utils/export.ts index c1e492933d1..a7625971516 100644 --- a/packages/remix-dev/codemod/replace-remix-magic-imports/utils/export.ts +++ b/packages/remix-dev/codemod/replace-remix-magic-imports/utils/export.ts @@ -216,7 +216,6 @@ const exportsByRenderer: Record> = { "useResolvedPath", "useSearchParams", "useSubmit", - "useTransition", ], }, }; diff --git a/packages/remix-eslint-config/rules/packageExports.js b/packages/remix-eslint-config/rules/packageExports.js index 92ce1b50cad..827df215954 100644 --- a/packages/remix-eslint-config/rules/packageExports.js +++ b/packages/remix-eslint-config/rules/packageExports.js @@ -134,7 +134,6 @@ const reactSpecificExports = { "useResolvedPath", "useSearchParams", "useSubmit", - "useTransition", ], type: [ "FormEncType", diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index ccbe94db44c..8c08e56efd8 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -7,7 +7,6 @@ import * as React from "react"; import type { AgnosticDataRouteMatch, UNSAFE_DeferredData as DeferredData, - Navigation, TrackedPromise, } from "@remix-run/router"; import type { @@ -68,14 +67,12 @@ import type { V2_MetaMatches, } from "./routeModules"; import type { - Transition, Fetcher, FetcherStates, LoaderSubmission, ActionSubmission, - TransitionStates, } from "./transition"; -import { IDLE_TRANSITION, IDLE_FETCHER } from "./transition"; +import { IDLE_FETCHER } from "./transition"; import { logDeprecationOnce } from "./warnings"; function useDataRouterContext() { @@ -377,12 +374,6 @@ let linksWarning = "For instructions on making this change see " + "https://remix.run/docs/en/v1.15.0/pages/v2#links-imagesizes-and-imagesrcset"; -let useTransitionWarning = - "⚠️ REMIX FUTURE CHANGE: `useTransition` will be removed in v2 in favor of `useNavigation`. " + - "You can prepare for this change at your convenience by updating to `useNavigation`. " + - "For instructions on making this change see " + - "https://remix.run/docs/en/v1.15.0/pages/v2#usetransition"; - let fetcherTypeWarning = "⚠️ REMIX FUTURE CHANGE: `fetcher.type` will be removed in v2. " + "Please use `fetcher.state`, `fetcher.formData`, and `fetcher.data` to achieve the same UX. " + @@ -1347,183 +1338,6 @@ export function useActionData(): SerializeFrom | undefined { return useActionDataRR() as SerializeFrom | undefined; } -/** - * Returns everything you need to know about a page transition to build pending - * navigation indicators and optimistic UI on data mutations. - * - * @deprecated in favor of useNavigation - * - * @see https://remix.run/hooks/use-transition - */ -export function useTransition(): Transition { - let navigation = useNavigation(); - - React.useEffect(() => { - logDeprecationOnce(useTransitionWarning); - }, []); - - return React.useMemo( - () => convertNavigationToTransition(navigation), - [navigation] - ); -} - -function convertNavigationToTransition(navigation: Navigation): Transition { - let { location, state, formMethod, formAction, formEncType, formData } = - navigation; - - if (!location) { - return IDLE_TRANSITION; - } - - let isActionSubmission = - formMethod != null && - ["POST", "PUT", "PATCH", "DELETE"].includes(formMethod.toUpperCase()); - - if ( - state === "submitting" && - formMethod && - formAction && - formEncType && - formData - ) { - if (isActionSubmission) { - // Actively submitting to an action - let transition: TransitionStates["SubmittingAction"] = { - location, - state, - submission: { - method: formMethod.toUpperCase() as ActionSubmission["method"], - action: formAction, - encType: formEncType, - formData: formData, - key: "", - }, - type: "actionSubmission", - }; - return transition; - } else { - // @remix-run/router doesn't mark loader submissions as state: "submitting" - invariant( - false, - "Encountered an unexpected navigation scenario in useTransition()" - ); - } - } - - if (state === "loading") { - let { _isRedirect, _isFetchActionRedirect } = location.state || {}; - if (formMethod && formAction && formEncType && formData) { - if (!_isRedirect) { - if (isActionSubmission) { - // We're reloading the same location after an action submission - let transition: TransitionStates["LoadingAction"] = { - location, - state, - submission: { - method: formMethod.toUpperCase() as ActionSubmission["method"], - action: formAction, - encType: formEncType, - formData: formData, - key: "", - }, - type: "actionReload", - }; - return transition; - } else { - // The new router fixes a bug in useTransition where the submission - // "action" represents the request URL not the state of the in - // the DOM. Back-port it here to maintain behavior, but useNavigation - // will fix this bug. - let url = new URL(formAction, window.location.origin); - - // This typing override should be safe since this is only running for - // GET submissions and over in @remix-run/router we have an invariant - // if you have any non-string values in your FormData when we attempt - // to convert them to URLSearchParams - url.search = new URLSearchParams( - formData.entries() as unknown as [string, string][] - ).toString(); - - // Actively "submitting" to a loader - let transition: TransitionStates["SubmittingLoader"] = { - location, - state: "submitting", - submission: { - method: formMethod.toUpperCase() as LoaderSubmission["method"], - action: url.pathname + url.search, - encType: formEncType, - formData: formData, - key: "", - }, - type: "loaderSubmission", - }; - return transition; - } - } else { - // Redirecting after a submission - if (isActionSubmission) { - let transition: TransitionStates["LoadingActionRedirect"] = { - location, - state, - submission: { - method: formMethod.toUpperCase() as ActionSubmission["method"], - action: formAction, - encType: formEncType, - formData: formData, - key: "", - }, - type: "actionRedirect", - }; - return transition; - } else { - let transition: TransitionStates["LoadingLoaderSubmissionRedirect"] = - { - location, - state, - submission: { - method: formMethod.toUpperCase() as LoaderSubmission["method"], - action: formAction, - encType: formEncType, - formData: formData, - key: "", - }, - type: "loaderSubmissionRedirect", - }; - return transition; - } - } - } else if (_isRedirect) { - if (_isFetchActionRedirect) { - let transition: TransitionStates["LoadingFetchActionRedirect"] = { - location, - state, - submission: undefined, - type: "fetchActionRedirect", - }; - return transition; - } else { - let transition: TransitionStates["LoadingRedirect"] = { - location, - state, - submission: undefined, - type: "normalRedirect", - }; - return transition; - } - } - } - - // If no scenarios above match, then it's a normal load! - let transition: TransitionStates["Loading"] = { - location, - state: "loading", - submission: undefined, - type: "normalLoad", - }; - return transition; -} - /** * Provides all fetchers currently on the page. Useful for layouts and parent * routes that need to provide pending/optimistic UI regarding the fetch. diff --git a/packages/remix-react/index.tsx b/packages/remix-react/index.tsx index 2f9162d31fa..52434c7a4bd 100644 --- a/packages/remix-react/index.tsx +++ b/packages/remix-react/index.tsx @@ -62,7 +62,6 @@ export { NavLink, PrefetchPageLinks, LiveReload, - useTransition, useFetcher, useFetchers, useLoaderData, diff --git a/packages/remix-react/transition.ts b/packages/remix-react/transition.ts index ae12b614c9d..284e7d6bafa 100644 --- a/packages/remix-react/transition.ts +++ b/packages/remix-react/transition.ts @@ -1,4 +1,4 @@ -import type { Location, FormEncType } from "react-router-dom"; +import type { FormEncType } from "react-router-dom"; export interface Submission { action: string; @@ -16,65 +16,6 @@ export interface LoaderSubmission extends Submission { method: "GET"; } -export type TransitionStates = { - Idle: { - state: "idle"; - type: "idle"; - submission: undefined; - location: undefined; - }; - SubmittingAction: { - state: "submitting"; - type: "actionSubmission"; - submission: ActionSubmission; - location: Location; - }; - SubmittingLoader: { - state: "submitting"; - type: "loaderSubmission"; - submission: LoaderSubmission; - location: Location; - }; - LoadingLoaderSubmissionRedirect: { - state: "loading"; - type: "loaderSubmissionRedirect"; - submission: LoaderSubmission; - location: Location; - }; - LoadingAction: { - state: "loading"; - type: "actionReload"; - submission: ActionSubmission; - location: Location; - }; - LoadingActionRedirect: { - state: "loading"; - type: "actionRedirect"; - submission: ActionSubmission; - location: Location; - }; - LoadingFetchActionRedirect: { - state: "loading"; - type: "fetchActionRedirect"; - submission: undefined; - location: Location; - }; - LoadingRedirect: { - state: "loading"; - type: "normalRedirect"; - submission: undefined; - location: Location; - }; - Loading: { - state: "loading"; - type: "normalLoad"; - location: Location; - submission: undefined; - }; -}; - -export type Transition = TransitionStates[keyof TransitionStates]; - // Thanks https://github.com/sindresorhus/type-fest! type JsonObject = { [Key in string]: JsonValue } & { [Key in string]?: JsonValue | undefined; @@ -197,13 +138,6 @@ export type FetcherStates = { export type Fetcher = FetcherStates[keyof FetcherStates]; -export const IDLE_TRANSITION: TransitionStates["Idle"] = { - state: "idle", - submission: undefined, - location: undefined, - type: "idle", -}; - export const IDLE_FETCHER: FetcherStates["Idle"] = { state: "idle", type: "init", diff --git a/rollup.utils.js b/rollup.utils.js index 6f36b967720..e07046f969b 100644 --- a/rollup.utils.js +++ b/rollup.utils.js @@ -302,7 +302,6 @@ function getMagicExports(packageName) { "useLoaderData", "useMatches", "useSubmit", - "useTransition", // react-router-dom exports "Outlet",