From 714a2c5ca9ae9aef136264a5635398861640733e Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 25 Jun 2024 10:27:13 -0400 Subject: [PATCH] Remove future.v7_startTransition flag (#11696) * Remove future.v7_startTransition flag * Remove from docs --- .changeset/early-beds-obey.md | 6 + examples/view-transitions/src/main.tsx | 8 +- packages/react-router-dom/index.ts | 1 - .../dom/concurrent-mode-navigations-test.tsx | 32 +- .../dom/flush-sync-navigations-test.tsx | 16 +- .../react-router/__tests__/navigate-test.tsx | 727 ++++++------------ .../__tests__/router/fetchers-test.ts | 3 +- .../router/utils/data-router-setup.ts | 2 - packages/react-router/index.ts | 2 - packages/react-router/lib/components.tsx | 25 +- packages/react-router/lib/context.ts | 2 + packages/react-router/lib/dom/lib.tsx | 133 +--- packages/react-router/lib/dom/server.tsx | 5 +- packages/react-router/lib/dom/ssr/browser.tsx | 6 +- 14 files changed, 262 insertions(+), 706 deletions(-) create mode 100644 .changeset/early-beds-obey.md diff --git a/.changeset/early-beds-obey.md b/.changeset/early-beds-obey.md new file mode 100644 index 0000000000..bac0cbcbf6 --- /dev/null +++ b/.changeset/early-beds-obey.md @@ -0,0 +1,6 @@ +--- +"react-router-dom": major +"react-router": major +--- + +Remove `future.v7_startTransition` flag diff --git a/examples/view-transitions/src/main.tsx b/examples/view-transitions/src/main.tsx index bfe23d1fc0..2d6ce1789a 100644 --- a/examples/view-transitions/src/main.tsx +++ b/examples/view-transitions/src/main.tsx @@ -246,13 +246,7 @@ function NavImage({ src, idx }: { src: string; idx: number }) { const rootElement = document.getElementById("root") as HTMLElement; ReactDOMClient.createRoot(rootElement).render( - + ); diff --git a/packages/react-router-dom/index.ts b/packages/react-router-dom/index.ts index 780b3108e9..d142f68d0b 100644 --- a/packages/react-router-dom/index.ts +++ b/packages/react-router-dom/index.ts @@ -19,7 +19,6 @@ export type { unstable_DataStrategyMatch, ErrorResponse, Fetcher, - FutureConfig, Hash, IndexRouteObject, IndexRouteProps, diff --git a/packages/react-router/__tests__/dom/concurrent-mode-navigations-test.tsx b/packages/react-router/__tests__/dom/concurrent-mode-navigations-test.tsx index 2d20d74eff..ee923706de 100644 --- a/packages/react-router/__tests__/dom/concurrent-mode-navigations-test.tsx +++ b/packages/react-router/__tests__/dom/concurrent-mode-navigations-test.tsx @@ -117,7 +117,7 @@ describe("Handles concurrent mode features during navigations", () => { getComponents(); let { container } = render( - + } /> { getComponents(); let { container } = render( - + } /> { getComponents(); let { container } = render( - + } /> { ) ); - let { container } = render( - - ); + let { container } = render(); await assertNavigation(container, resolve, resolveLazy); }); @@ -296,7 +288,7 @@ describe("Handles concurrent mode features during navigations", () => { getComponents(); let { container } = render( - + } /> } /> @@ -314,10 +306,7 @@ describe("Handles concurrent mode features during navigations", () => { getComponents(); let { container } = render( - + } /> } /> @@ -335,10 +324,7 @@ describe("Handles concurrent mode features during navigations", () => { getComponents(); let { container } = render( - + } /> } /> @@ -364,9 +350,7 @@ describe("Handles concurrent mode features during navigations", () => { ) ); - let { container } = render( - - ); + let { container } = render(); await assertNavigation(container, resolve, resolveLazy); }); diff --git a/packages/react-router/__tests__/dom/flush-sync-navigations-test.tsx b/packages/react-router/__tests__/dom/flush-sync-navigations-test.tsx index 783f2eb597..2019b99f9e 100644 --- a/packages/react-router/__tests__/dom/flush-sync-navigations-test.tsx +++ b/packages/react-router/__tests__/dom/flush-sync-navigations-test.tsx @@ -48,9 +48,7 @@ describe("flushSync", () => { window: getWindowImpl("/"), } ); - render( - - ); + render(); // This isn't the best way to test this but it seems that startTransition is // performing sync updates in the test/JSDOM/whatever environment which is @@ -127,9 +125,7 @@ describe("flushSync", () => { window: getWindowImpl("/"), } ); - render( - - ); + render(); // This isn't the best way to test this but it seems that startTransition is // performing sync updates in the test/JSDOM/whatever environment which is @@ -191,9 +187,7 @@ describe("flushSync", () => { window: getWindowImpl("/"), } ); - render( - - ); + render(); // This isn't the best way to test this but it seems that startTransition is // performing sync updates in the test/JSDOM/whatever environment which is @@ -256,9 +250,7 @@ describe("flushSync", () => { window: getWindowImpl("/"), } ); - render( - - ); + render(); // This isn't the best way to test this but it seems that startTransition is // performing sync updates in the test/JSDOM/whatever environment which is diff --git a/packages/react-router/__tests__/navigate-test.tsx b/packages/react-router/__tests__/navigate-test.tsx index 8ec88ede60..6b95638227 100644 --- a/packages/react-router/__tests__/navigate-test.tsx +++ b/packages/react-router/__tests__/navigate-test.tsx @@ -568,52 +568,51 @@ describe("", () => { }); }); -describe("concurrent mode", () => { - describe("v7_startTransition = false", () => { - it("handles setState in render in StrictMode using a data router (sync loader)", async () => { - let renders: number[] = []; - const router = createMemoryRouter([ - { - path: "/", - children: [ - { - index: true, - Component() { - let [count, setCount] = React.useState(0); - if (count === 0) { - setCount(1); - } - return ; - }, +describe("concurrent mode (using React.startTransition for updates)", () => { + it("handles setState in render in StrictMode using a data router (sync loader)", async () => { + let renders: number[] = []; + const router = createMemoryRouter([ + { + path: "/", + children: [ + { + index: true, + Component() { + let [count, setCount] = React.useState(0); + if (count === 0) { + setCount(1); + } + return ; }, - { - path: "b", - Component() { - let { state } = useLocation() as { state: { count: number } }; - renders.push(state.count); - return ( - <> -

Page B

-

{state.count}

- - ); - }, + }, + { + path: "b", + Component() { + let { state } = useLocation() as { state: { count: number } }; + renders.push(state.count); + return ( + <> +

Page B

+

{state.count}

+ + ); }, - ], - }, - ]); + }, + ], + }, + ]); - let navigateSpy = jest.spyOn(router, "navigate"); + let navigateSpy = jest.spyOn(router, "navigate"); - let { container } = render( - - - - ); + let { container } = render( + + + + ); - await waitFor(() => screen.getByText("Page B")); + await waitFor(() => screen.getByText("Page B")); - expect(getHtml(container)).toMatchInlineSnapshot(` + expect(getHtml(container)).toMatchInlineSnapshot(` "

Page B @@ -623,208 +622,64 @@ describe("concurrent mode", () => {

" `); - expect(navigateSpy).toHaveBeenCalledTimes(2); - expect(navigateSpy.mock.calls[0]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - expect(navigateSpy.mock.calls[1]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - expect(renders).toEqual([1, 1]); - }); - - it("handles setState in effect in StrictMode using a data router (sync loader)", async () => { - let renders: number[] = []; - const router = createMemoryRouter([ - { - path: "/", - children: [ - { - index: true, - Component() { - let [count, setCount] = React.useState(0); - React.useEffect(() => { - if (count === 0) { - setCount(1); - } - }, [count]); - return ; - }, - }, - { - path: "b", - Component() { - let { state } = useLocation() as { state: { count: number } }; - renders.push(state.count); - return ( - <> -

Page B

-

{state.count}

- - ); - }, - }, - ], - }, - ]); - - let navigateSpy = jest.spyOn(router, "navigate"); - - let { container } = render( - - - - ); - - await waitFor(() => screen.getByText("Page B")); - - expect(getHtml(container)).toMatchInlineSnapshot(` - "
-

- Page B -

-

- 0 -

-
" - `); - expect(navigateSpy).toHaveBeenCalledTimes(2); - expect(navigateSpy.mock.calls[0]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 0 } }, - ]); - expect(navigateSpy.mock.calls[1]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 0 } }, - ]); - expect(renders).toEqual([0, 0]); - }); + expect(navigateSpy).toHaveBeenCalledTimes(2); + expect(navigateSpy.mock.calls[0]).toMatchObject([ + { pathname: "/b" }, + { state: { count: 1 } }, + ]); + expect(navigateSpy.mock.calls[1]).toMatchObject([ + { pathname: "/b" }, + { state: { count: 1 } }, + ]); + expect(renders).toEqual([1, 1]); + }); - it("handles setState in render in StrictMode using a data router (async loader)", async () => { - let renders: number[] = []; - const router = createMemoryRouter([ - { - path: "/", - children: [ - { - index: true, - Component() { - let [count, setCount] = React.useState(0); + it("handles setState in effect in StrictMode using a data router (sync loader)", async () => { + let renders: number[] = []; + const router = createMemoryRouter([ + { + path: "/", + children: [ + { + index: true, + Component() { + let [count, setCount] = React.useState(0); + React.useEffect(() => { if (count === 0) { setCount(1); } - return ; - }, - }, - { - path: "b", - async loader() { - await new Promise((r) => setTimeout(r, 10)); - return null; - }, - Component() { - let { state } = useLocation() as { state: { count: number } }; - renders.push(state.count); - return ( - <> -

Page B

-

{state.count}

- - ); - }, - }, - ], - }, - ]); - - let navigateSpy = jest.spyOn(router, "navigate"); - - let { container } = render( - - - - ); - - await waitFor(() => screen.getByText("Page B")); - - expect(getHtml(container)).toMatchInlineSnapshot(` - "
-

- Page B -

-

- 1 -

-
" - `); - expect(navigateSpy).toHaveBeenCalledTimes(2); - expect(navigateSpy.mock.calls[0]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - expect(navigateSpy.mock.calls[1]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - // /a/b rendered with the same state value both times - expect(renders).toEqual([1, 1]); - }); - - it("handles setState in effect in StrictMode using a data router (async loader)", async () => { - let renders: number[] = []; - const router = createMemoryRouter([ - { - path: "/", - children: [ - { - index: true, - Component() { - // When state managed by react and changes during render, we'll - // only "see" the value from the first pass through here in our - // effects - let [count, setCount] = React.useState(0); - React.useEffect(() => { - if (count === 0) { - setCount(1); - } - }, [count]); - return ; - }, + }, [count]); + return ; }, - { - path: "b", - async loader() { - await new Promise((r) => setTimeout(r, 10)); - return null; - }, - Component() { - let { state } = useLocation() as { state: { count: number } }; - renders.push(state.count); - return ( - <> -

Page B

-

{state.count}

- - ); - }, + }, + { + path: "b", + Component() { + let { state } = useLocation() as { state: { count: number } }; + renders.push(state.count); + return ( + <> +

Page B

+

{state.count}

+ + ); }, - ], - }, - ]); + }, + ], + }, + ]); - let navigateSpy = jest.spyOn(router, "navigate"); + let navigateSpy = jest.spyOn(router, "navigate"); - let { container } = render( - - - - ); + let { container } = render( + + + + ); - await waitFor(() => screen.getByText("Page B")); + await waitFor(() => screen.getByText("Page B")); - expect(getHtml(container)).toMatchInlineSnapshot(` + expect(getHtml(container)).toMatchInlineSnapshot(` "

Page B @@ -834,144 +689,70 @@ describe("concurrent mode", () => {

" `); - expect(navigateSpy).toHaveBeenCalledTimes(3); - expect(navigateSpy.mock.calls[0]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 0 } }, - ]); - expect(navigateSpy.mock.calls[1]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 0 } }, - ]); - // StrictMode only applies the double-effect execution on component mount, - // not component update - expect(navigateSpy.mock.calls[2]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - // /a/b rendered with the latest state value both times - expect(renders).toEqual([1, 1]); - }); + expect(navigateSpy).toHaveBeenCalledTimes(3); + expect(navigateSpy.mock.calls[0]).toMatchObject([ + { pathname: "/b" }, + { state: { count: 0 } }, + ]); + expect(navigateSpy.mock.calls[1]).toMatchObject([ + { pathname: "/b" }, + { state: { count: 0 } }, + ]); + expect(navigateSpy.mock.calls[2]).toMatchObject([ + { pathname: "/b" }, + { state: { count: 1 } }, + ]); + expect(renders).toEqual([1, 1]); }); - describe("v7_startTransition = true", () => { - it("handles setState in render in StrictMode using a data router (sync loader)", async () => { - let renders: number[] = []; - const router = createMemoryRouter([ - { - path: "/", - children: [ - { - index: true, - Component() { - let [count, setCount] = React.useState(0); - if (count === 0) { - setCount(1); - } - return ; - }, - }, - { - path: "b", - Component() { - let { state } = useLocation() as { state: { count: number } }; - renders.push(state.count); - return ( - <> -

Page B

-

{state.count}

- - ); - }, + it("handles setState in render in StrictMode using a data router (async loader)", async () => { + let renders: number[] = []; + const router = createMemoryRouter([ + { + path: "/", + children: [ + { + index: true, + Component() { + let [count, setCount] = React.useState(0); + if (count === 0) { + setCount(1); + } + return ; }, - ], - }, - ]); - - let navigateSpy = jest.spyOn(router, "navigate"); - - let { container } = render( - - - - ); - - await waitFor(() => screen.getByText("Page B")); - - expect(getHtml(container)).toMatchInlineSnapshot(` - "
-

- Page B -

-

- 1 -

-
" - `); - expect(navigateSpy).toHaveBeenCalledTimes(2); - expect(navigateSpy.mock.calls[0]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - expect(navigateSpy.mock.calls[1]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - expect(renders).toEqual([1, 1]); - }); - - it("handles setState in effect in StrictMode using a data router (sync loader)", async () => { - let renders: number[] = []; - const router = createMemoryRouter([ - { - path: "/", - children: [ - { - index: true, - Component() { - let [count, setCount] = React.useState(0); - React.useEffect(() => { - if (count === 0) { - setCount(1); - } - }, [count]); - return ; - }, + }, + { + path: "b", + async loader() { + await new Promise((r) => setTimeout(r, 10)); + return null; }, - { - path: "b", - Component() { - let { state } = useLocation() as { state: { count: number } }; - renders.push(state.count); - return ( - <> -

Page B

-

{state.count}

- - ); - }, + Component() { + let { state } = useLocation() as { state: { count: number } }; + renders.push(state.count); + return ( + <> +

Page B

+

{state.count}

+ + ); }, - ], - }, - ]); + }, + ], + }, + ]); - let navigateSpy = jest.spyOn(router, "navigate"); + let navigateSpy = jest.spyOn(router, "navigate"); - let { container } = render( - - - - ); + let { container } = render( + + + + ); - await waitFor(() => screen.getByText("Page B")); + await waitFor(() => screen.getByText("Page B")); - expect(getHtml(container)).toMatchInlineSnapshot(` + expect(getHtml(container)).toMatchInlineSnapshot(` "

Page B @@ -981,151 +762,72 @@ describe("concurrent mode", () => {

" `); - expect(navigateSpy).toHaveBeenCalledTimes(3); - expect(navigateSpy.mock.calls[0]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 0 } }, - ]); - expect(navigateSpy.mock.calls[1]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 0 } }, - ]); - expect(navigateSpy.mock.calls[2]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - expect(renders).toEqual([1, 1]); - }); + expect(navigateSpy).toHaveBeenCalledTimes(2); + expect(navigateSpy.mock.calls[0]).toMatchObject([ + { pathname: "/b" }, + { state: { count: 1 } }, + ]); + expect(navigateSpy.mock.calls[1]).toMatchObject([ + { pathname: "/b" }, + { state: { count: 1 } }, + ]); + // /a/b rendered with the same state value both times + expect(renders).toEqual([1, 1]); + }); - it("handles setState in render in StrictMode using a data router (async loader)", async () => { - let renders: number[] = []; - const router = createMemoryRouter([ - { - path: "/", - children: [ - { - index: true, - Component() { - let [count, setCount] = React.useState(0); + it("handles setState in effect in StrictMode using a data router (async loader)", async () => { + let renders: number[] = []; + const router = createMemoryRouter([ + { + path: "/", + children: [ + { + index: true, + Component() { + // When state managed by react and changes during render, we'll + // only "see" the value from the first pass through here in our + // effects + let [count, setCount] = React.useState(0); + React.useEffect(() => { if (count === 0) { setCount(1); } - return ; - }, - }, - { - path: "b", - async loader() { - await new Promise((r) => setTimeout(r, 10)); - return null; - }, - Component() { - let { state } = useLocation() as { state: { count: number } }; - renders.push(state.count); - return ( - <> -

Page B

-

{state.count}

- - ); - }, + }, [count]); + return ; }, - ], - }, - ]); - - let navigateSpy = jest.spyOn(router, "navigate"); - - let { container } = render( - - - - ); - - await waitFor(() => screen.getByText("Page B")); - - expect(getHtml(container)).toMatchInlineSnapshot(` - "
-

- Page B -

-

- 1 -

-
" - `); - expect(navigateSpy).toHaveBeenCalledTimes(2); - expect(navigateSpy.mock.calls[0]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - expect(navigateSpy.mock.calls[1]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - // /a/b rendered with the same state value both times - expect(renders).toEqual([1, 1]); - }); - - it("handles setState in effect in StrictMode using a data router (async loader)", async () => { - let renders: number[] = []; - const router = createMemoryRouter([ - { - path: "/", - children: [ - { - index: true, - Component() { - // When state managed by react and changes during render, we'll - // only "see" the value from the first pass through here in our - // effects - let [count, setCount] = React.useState(0); - React.useEffect(() => { - if (count === 0) { - setCount(1); - } - }, [count]); - return ; - }, + }, + { + path: "b", + async loader() { + await new Promise((r) => setTimeout(r, 10)); + return null; }, - { - path: "b", - async loader() { - await new Promise((r) => setTimeout(r, 10)); - return null; - }, - Component() { - let { state } = useLocation() as { state: { count: number } }; - renders.push(state.count); - return ( - <> -

Page B

-

{state.count}

- - ); - }, + Component() { + let { state } = useLocation() as { state: { count: number } }; + renders.push(state.count); + return ( + <> +

Page B

+

{state.count}

+ + ); }, - ], - }, - ]); + }, + ], + }, + ]); - let navigateSpy = jest.spyOn(router, "navigate"); + let navigateSpy = jest.spyOn(router, "navigate"); - let { container } = render( - - - - ); + let { container } = render( + + + + ); - await waitFor(() => screen.getByText("Page B")); + await waitFor(() => screen.getByText("Page B")); - expect(getHtml(container)).toMatchInlineSnapshot(` + expect(getHtml(container)).toMatchInlineSnapshot(` "

Page B @@ -1135,23 +837,22 @@ describe("concurrent mode", () => {

" `); - expect(navigateSpy).toHaveBeenCalledTimes(3); - expect(navigateSpy.mock.calls[0]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 0 } }, - ]); - expect(navigateSpy.mock.calls[1]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 0 } }, - ]); - // StrictMode only applies the double-effect execution on component mount, - // not component update - expect(navigateSpy.mock.calls[2]).toMatchObject([ - { pathname: "/b" }, - { state: { count: 1 } }, - ]); - // /a/b rendered with the latest state value both times - expect(renders).toEqual([1, 1]); - }); + expect(navigateSpy).toHaveBeenCalledTimes(3); + expect(navigateSpy.mock.calls[0]).toMatchObject([ + { pathname: "/b" }, + { state: { count: 0 } }, + ]); + expect(navigateSpy.mock.calls[1]).toMatchObject([ + { pathname: "/b" }, + { state: { count: 0 } }, + ]); + // StrictMode only applies the double-effect execution on component mount, + // not component update + expect(navigateSpy.mock.calls[2]).toMatchObject([ + { pathname: "/b" }, + { state: { count: 1 } }, + ]); + // /a/b rendered with the latest state value both times + expect(renders).toEqual([1, 1]); }); }); diff --git a/packages/react-router/__tests__/router/fetchers-test.ts b/packages/react-router/__tests__/router/fetchers-test.ts index 8592009e60..504e68d750 100644 --- a/packages/react-router/__tests__/router/fetchers-test.ts +++ b/packages/react-router/__tests__/router/fetchers-test.ts @@ -1,5 +1,5 @@ /* eslint-disable jest/valid-title */ -import type { FutureConfig, HydrationState } from "../../lib/router"; +import type { HydrationState } from "../../lib/router"; import { createMemoryHistory, createRouter, @@ -19,7 +19,6 @@ import { createFormData, tick } from "./utils/utils"; function initializeTest(init?: { url?: string; hydrationData?: HydrationState; - future?: Partial; }) { return setup({ routes: [ diff --git a/packages/react-router/__tests__/router/utils/data-router-setup.ts b/packages/react-router/__tests__/router/utils/data-router-setup.ts index 6a6a09358d..7dd6288328 100644 --- a/packages/react-router/__tests__/router/utils/data-router-setup.ts +++ b/packages/react-router/__tests__/router/utils/data-router-setup.ts @@ -7,7 +7,6 @@ import type { InitialEntry, Router, RouterNavigateOptions, - FutureConfig, } from "../../../lib/router"; import { createMemoryHistory, @@ -143,7 +142,6 @@ type SetupOpts = { initialIndex?: number; hydrationData?: HydrationState; dataStrategy?: DataStrategyFunction; - future?: Partial; }; // We use a slightly modified version of createDeferred here that includes the diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index a699d6fb27..2fbb5905fd 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -48,7 +48,6 @@ import { import type { AwaitProps, - FutureConfig, IndexRouteProps, LayoutRouteProps, MemoryRouterProps, @@ -134,7 +133,6 @@ export type { unstable_DataStrategyMatch, ErrorResponse, Fetcher, - FutureConfig, Hash, IndexRouteObject, IndexRouteProps, diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 13d53acd45..2528553e6b 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -47,17 +47,12 @@ import { useOutlet, useRoutes, } from "./hooks"; -import { startTransitionSafe } from "./dom/lib"; import { getResolveToMatches } from "./router/utils"; // TODO: Let's get this back to using an import map and development/production // condition once we get the rollup build replaced const ENABLE_DEV_WARNINGS = true; -export interface FutureConfig { - v7_startTransition: boolean; -} - /** * @private */ @@ -166,7 +161,6 @@ export interface MemoryRouterProps { children?: React.ReactNode; initialEntries?: InitialEntry[]; initialIndex?: number; - future?: Partial; } /** @@ -179,7 +173,6 @@ export function MemoryRouter({ children, initialEntries, initialIndex, - future, }: MemoryRouterProps): React.ReactElement { let historyRef = React.useRef(); if (historyRef.current == null) { @@ -195,14 +188,11 @@ export function MemoryRouter({ action: history.action, location: history.location, }); - let { v7_startTransition } = future || {}; let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - v7_startTransition - ? startTransitionSafe(() => setStateImpl(newState)) - : setStateImpl(newState); + React.startTransition(() => setStateImpl(newState)); }, - [setStateImpl, v7_startTransition] + [setStateImpl] ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -214,7 +204,6 @@ export function MemoryRouter({ location={state.location} navigationType={state.action} navigator={history} - future={future} /> ); } @@ -251,7 +240,7 @@ export function Navigate({ ` may be used only in the context of a component.` ); - let { future, static: isStatic } = React.useContext(NavigationContext); + let { static: isStatic } = React.useContext(NavigationContext); warning( !isStatic, @@ -398,7 +387,6 @@ export interface RouterProps { navigationType?: NavigationType; navigator: Navigator; static?: boolean; - future?: Partial; } /** @@ -417,7 +405,6 @@ export function Router({ navigationType = NavigationType.Pop, navigator, static: staticProp = false, - future, }: RouterProps): React.ReactElement | null { invariant( !useInRouterContext(), @@ -433,11 +420,9 @@ export function Router({ basename, navigator, static: staticProp, - future: { - ...future, - }, + future: {}, }), - [basename, future, navigator, staticProp] + [basename, navigator, staticProp] ); if (typeof locationProp === "string") { diff --git a/packages/react-router/lib/context.ts b/packages/react-router/lib/context.ts index 0c52747586..e6f35b7794 100644 --- a/packages/react-router/lib/context.ts +++ b/packages/react-router/lib/context.ts @@ -121,6 +121,8 @@ interface NavigationContextObject { basename: string; navigator: Navigator; static: boolean; + // TODO: Re-introduce a singular `FutureConfig` once we land our first + // future.unstable_ or future.v8_ flag future: {}; } diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index c78ff22c02..fbb27ed1b8 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -64,11 +64,7 @@ import { mergeRefs, usePrefetchBehavior, } from "./ssr/components"; -import type { - FutureConfig, - FutureConfig as RenderFutureConfig, - unstable_PatchRoutesOnMissFunction, -} from "../components"; +import type { unstable_PatchRoutesOnMissFunction } from "../components"; import { Router, mapRouteProperties } from "../components"; import type { Navigator, @@ -278,50 +274,6 @@ export { FetchersContext as UNSAFE_FetchersContext }; //#region Components //////////////////////////////////////////////////////////////////////////////// -/** - Webpack + React 17 fails to compile on any of the following because webpack - complains that `startTransition` doesn't exist in `React`: - * import { startTransition } from "react" - * import * as React from from "react"; - "startTransition" in React ? React.startTransition(() => setState()) : setState() - * import * as React from from "react"; - "startTransition" in React ? React["startTransition"](() => setState()) : setState() - - Moving it to a constant such as the following solves the Webpack/React 17 issue: - * import * as React from from "react"; - const START_TRANSITION = "startTransition"; - START_TRANSITION in React ? React[START_TRANSITION](() => setState()) : setState() - - However, that introduces webpack/terser minification issues in production builds - in React 18 where minification/obfuscation ends up removing the call of - React.startTransition entirely from the first half of the ternary. Grabbing - this exported reference once up front resolves that issue. - - See https://github.com/remix-run/react-router/issues/10579 -*/ -const START_TRANSITION = "startTransition"; -const startTransitionImpl = React[START_TRANSITION]; -const FLUSH_SYNC = "flushSync"; -const flushSyncImpl = ReactDOM[FLUSH_SYNC]; -const USE_ID = "useId"; -const useIdImpl = React[USE_ID]; - -export function startTransitionSafe(cb: () => void) { - if (startTransitionImpl) { - startTransitionImpl(cb); - } else { - cb(); - } -} - -function flushSyncSafe(cb: () => void) { - if (flushSyncImpl) { - flushSyncImpl(cb); - } else { - cb(); - } -} - export interface ViewTransition { finished: Promise; ready: Promise; @@ -357,9 +309,6 @@ class Deferred { export interface RouterProviderProps { fallbackElement?: React.ReactNode; router: RemixRouter; - // Only accept future flags relevant to rendering behavior - // routing flags should be accessed via router.future - future?: Partial>; } /** @@ -368,7 +317,6 @@ export interface RouterProviderProps { export function RouterProvider({ fallbackElement, router, - future, }: RouterProviderProps): React.ReactElement { let [state, setStateImpl] = React.useState(router.state); let [pendingState, setPendingState] = React.useState(); @@ -383,18 +331,6 @@ export function RouterProvider({ nextLocation: Location; }>(); let fetcherData = React.useRef>(new Map()); - let { v7_startTransition } = future || {}; - - let optInStartTransition = React.useCallback( - (cb: () => void) => { - if (v7_startTransition) { - startTransitionSafe(cb); - } else { - cb(); - } - }, - [v7_startTransition] - ); let setState = React.useCallback( ( @@ -421,9 +357,9 @@ export function RouterProvider({ // just update and be done with it if (!viewTransitionOpts || isViewTransitionUnavailable) { if (flushSync) { - flushSyncSafe(() => setStateImpl(newState)); + ReactDOM.flushSync(() => setStateImpl(newState)); } else { - optInStartTransition(() => setStateImpl(newState)); + React.startTransition(() => setStateImpl(newState)); } return; } @@ -431,7 +367,7 @@ export function RouterProvider({ // flushSync + startViewTransition if (flushSync) { // Flush through the context to mark DOM elements as transition=ing - flushSyncSafe(() => { + ReactDOM.flushSync(() => { // Cancel any pending transitions if (transition) { renderDfd && renderDfd.resolve(); @@ -447,12 +383,12 @@ export function RouterProvider({ // Update the DOM let t = router.window!.document.startViewTransition(() => { - flushSyncSafe(() => setStateImpl(newState)); + ReactDOM.flushSync(() => setStateImpl(newState)); }); // Clean up after the animation completes t.finished.finally(() => { - flushSyncSafe(() => { + ReactDOM.flushSync(() => { setRenderDfd(undefined); setTransition(undefined); setPendingState(undefined); @@ -460,7 +396,7 @@ export function RouterProvider({ }); }); - flushSyncSafe(() => setTransition(t)); + ReactDOM.flushSync(() => setTransition(t)); return; } @@ -486,7 +422,7 @@ export function RouterProvider({ }); } }, - [router.window, transition, renderDfd, fetcherData, optInStartTransition] + [router.window, transition, renderDfd, fetcherData] ); // Need to use a layout effect here so we are subscribed early enough to @@ -509,7 +445,7 @@ export function RouterProvider({ let newState = pendingState; let renderPromise = renderDfd.promise; let transition = router.window.document.startViewTransition(async () => { - optInStartTransition(() => setStateImpl(newState)); + React.startTransition(() => setStateImpl(newState)); await renderPromise; }); transition.finished.finally(() => { @@ -520,7 +456,7 @@ export function RouterProvider({ }); setTransition(transition); } - }, [optInStartTransition, pendingState, renderDfd, router.window]); + }, [pendingState, renderDfd, router.window]); // When the new location finally renders and is committed to the DOM, this // effect will run to resolve the transition @@ -645,7 +581,6 @@ function DataRoutes({ export interface BrowserRouterProps { basename?: string; children?: React.ReactNode; - future?: Partial; window?: Window; } @@ -657,7 +592,6 @@ export interface BrowserRouterProps { export function BrowserRouter({ basename, children, - future, window, }: BrowserRouterProps) { let historyRef = React.useRef(); @@ -670,14 +604,11 @@ export function BrowserRouter({ action: history.action, location: history.location, }); - let { v7_startTransition } = future || {}; let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - v7_startTransition && startTransitionImpl - ? startTransitionImpl(() => setStateImpl(newState)) - : setStateImpl(newState); + React.startTransition(() => setStateImpl(newState)); }, - [setStateImpl, v7_startTransition] + [setStateImpl] ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -689,7 +620,6 @@ export function BrowserRouter({ location={state.location} navigationType={state.action} navigator={history} - future={future} /> ); } @@ -700,7 +630,6 @@ export function BrowserRouter({ export interface HashRouterProps { basename?: string; children?: React.ReactNode; - future?: Partial; window?: Window; } @@ -710,12 +639,7 @@ export interface HashRouterProps { * * @category Router Components */ -export function HashRouter({ - basename, - children, - future, - window, -}: HashRouterProps) { +export function HashRouter({ basename, children, window }: HashRouterProps) { let historyRef = React.useRef(); if (historyRef.current == null) { historyRef.current = createHashHistory({ window, v5Compat: true }); @@ -726,14 +650,11 @@ export function HashRouter({ action: history.action, location: history.location, }); - let { v7_startTransition } = future || {}; let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - v7_startTransition && startTransitionImpl - ? startTransitionImpl(() => setStateImpl(newState)) - : setStateImpl(newState); + React.startTransition(() => setStateImpl(newState)); }, - [setStateImpl, v7_startTransition] + [setStateImpl] ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -745,7 +666,6 @@ export function HashRouter({ location={state.location} navigationType={state.action} navigator={history} - future={future} /> ); } @@ -756,7 +676,6 @@ export function HashRouter({ export interface HistoryRouterProps { basename?: string; children?: React.ReactNode; - future?: RenderFutureConfig; history: History; } @@ -768,24 +687,16 @@ export interface HistoryRouterProps { * * @category Router Components */ -function HistoryRouter({ - basename, - children, - future, - history, -}: HistoryRouterProps) { +function HistoryRouter({ basename, children, history }: HistoryRouterProps) { let [state, setStateImpl] = React.useState({ action: history.action, location: history.location, }); - let { v7_startTransition } = future || {}; let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - v7_startTransition && startTransitionImpl - ? startTransitionImpl(() => setStateImpl(newState)) - : setStateImpl(newState); + React.startTransition(() => setStateImpl(newState)); }, - [setStateImpl, v7_startTransition] + [setStateImpl] ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -797,7 +708,6 @@ function HistoryRouter({ location={state.location} navigationType={state.action} navigator={history} - future={future} /> ); } @@ -2229,15 +2139,10 @@ export function useFetcher({ ); // Fetcher key handling - // OK to call conditionally to feature detect `useId` - // eslint-disable-next-line react-hooks/rules-of-hooks - let defaultKey = useIdImpl ? useIdImpl() : ""; + let defaultKey = React.useId(); let [fetcherKey, setFetcherKey] = React.useState(key || defaultKey); if (key && key !== fetcherKey) { setFetcherKey(key); - } else if (!fetcherKey) { - // We will only fall through here when `useId` is not available - setFetcherKey(getUniqueFetcherId()); } // Registration/cleanup diff --git a/packages/react-router/lib/dom/server.tsx b/packages/react-router/lib/dom/server.tsx index cf1cfd7ef3..3530bfbe02 100644 --- a/packages/react-router/lib/dom/server.tsx +++ b/packages/react-router/lib/dom/server.tsx @@ -28,7 +28,7 @@ import { UNSAFE_FetchersContext as FetchersContext, UNSAFE_ViewTransitionContext as ViewTransitionContext, } from "./lib"; -import { Router, mapRouteProperties, type FutureConfig } from "../components"; +import { Router, mapRouteProperties } from "../components"; import type { DataRouteObject, RouteObject } from "../context"; import { DataRouterContext, DataRouterStateContext } from "../context"; import { useRoutesImpl } from "../hooks"; @@ -37,7 +37,6 @@ export interface StaticRouterProps { basename?: string; children?: React.ReactNode; location: Partial | string; - future?: Partial; } /** @@ -50,7 +49,6 @@ export function StaticRouter({ basename, children, location: locationProp = "/", - future, }: StaticRouterProps) { if (typeof locationProp === "string") { locationProp = parsePath(locationProp); @@ -73,7 +71,6 @@ export function StaticRouter({ location={location} navigationType={action} navigator={staticNavigator} - future={future} static={true} /> ); diff --git a/packages/react-router/lib/dom/ssr/browser.tsx b/packages/react-router/lib/dom/ssr/browser.tsx index 51cfeeb244..14d31e7a25 100644 --- a/packages/react-router/lib/dom/ssr/browser.tsx +++ b/packages/react-router/lib/dom/ssr/browser.tsx @@ -255,11 +255,7 @@ export function HydratedRouter() { }} > - + {/*