From ae0067bf136a05f7bed8bf9164e8041d3efc1844 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 12 Jun 2023 11:22:01 -0400 Subject: [PATCH 1/9] Enter prerelease mode --- .changeset/pre.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..c64da1a8af --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,12 @@ +{ + "mode": "pre", + "tag": "pre", + "initialVersions": { + "react-router": "6.12.1", + "react-router-dom": "6.12.1", + "react-router-dom-v5-compat": "6.12.1", + "react-router-native": "6.12.1", + "@remix-run/router": "1.6.3" + }, + "changesets": [] +} From 2f79bcef5f396943cf95d0d23fbd67df9b8e17a6 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 12 Jun 2023 12:09:46 -0400 Subject: [PATCH 2/9] Fix webpack/terser startTransition minification bug in production mode (#10588) --- .changeset/start-transition-minification.md | 6 ++++ packages/react-router-dom/index.tsx | 32 +++++++++++++++------ packages/react-router/lib/components.tsx | 28 +++++++++++++----- 3 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 .changeset/start-transition-minification.md diff --git a/.changeset/start-transition-minification.md b/.changeset/start-transition-minification.md new file mode 100644 index 0000000000..79ae098c06 --- /dev/null +++ b/.changeset/start-transition-minification.md @@ -0,0 +1,6 @@ +--- +"react-router": patch +"react-router-dom": patch +--- + +Work around webpack/terser `React.startTransition` minification bug in production mode diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index e7a26d4233..d2356a84f0 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -300,10 +300,24 @@ export interface BrowserRouterProps { window?: Window; } -// Webpack + React 17 fails to compile on the usage of `React.startTransition` or -// `React["startTransition"]` even if it's behind a feature detection of -// `"startTransition" in React`. Moving this to a constant avoids the issue :/ +// Webpack + React 17 fails to compile on any of the following: +// * 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 reference once up front resolves that issue. const START_TRANSITION = "startTransition"; +const startTransitionImpl = React[START_TRANSITION]; /** * A `` for use in web browsers. Provides the cleanest URLs. @@ -325,8 +339,8 @@ export function BrowserRouter({ }); let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - START_TRANSITION in React - ? React[START_TRANSITION](() => setStateImpl(newState)) + startTransitionImpl + ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); }, [setStateImpl] @@ -368,8 +382,8 @@ export function HashRouter({ basename, children, window }: HashRouterProps) { }); let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - START_TRANSITION in React - ? React[START_TRANSITION](() => setStateImpl(newState)) + startTransitionImpl + ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); }, [setStateImpl] @@ -407,8 +421,8 @@ function HistoryRouter({ basename, children, history }: HistoryRouterProps) { }); let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - START_TRANSITION in React - ? React[START_TRANSITION](() => setStateImpl(newState)) + startTransitionImpl + ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); }, [setStateImpl] diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 6c62276c92..e4d4eee360 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -54,10 +54,24 @@ export interface RouterProviderProps { router: RemixRouter; } -// Webpack + React 17 fails to compile on the usage of `React.startTransition` or -// `React["startTransition"]` even if it's behind a feature detection of -// `"startTransition" in React`. Moving this to a constant avoids the issue :/ +// Webpack + React 17 fails to compile on any of the following: +// * 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 reference once up front resolves that issue. const START_TRANSITION = "startTransition"; +const startTransitionImpl = React[START_TRANSITION]; /** * Given a Remix Router instance, render the appropriate UI @@ -71,8 +85,8 @@ export function RouterProvider({ let [state, setStateImpl] = React.useState(router.state); let setState = React.useCallback( (newState: RouterState) => { - START_TRANSITION in React - ? React[START_TRANSITION](() => setStateImpl(newState)) + startTransitionImpl + ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); }, [setStateImpl] @@ -183,8 +197,8 @@ export function MemoryRouter({ }); let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - START_TRANSITION in React - ? React[START_TRANSITION](() => setStateImpl(newState)) + startTransitionImpl + ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); }, [setStateImpl] From 31bdd23c3f2fe1bdd4154187764ec5e14a3c3045 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 12:11:20 -0400 Subject: [PATCH 3/9] chore: Update version for release (pre) (#10589) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 4 +++- packages/react-router-dom-v5-compat/CHANGELOG.md | 8 ++++++++ packages/react-router-dom-v5-compat/package.json | 4 ++-- packages/react-router-dom/CHANGELOG.md | 8 ++++++++ packages/react-router-dom/package.json | 4 ++-- packages/react-router-native/CHANGELOG.md | 7 +++++++ packages/react-router-native/package.json | 4 ++-- packages/react-router/CHANGELOG.md | 6 ++++++ packages/react-router/package.json | 2 +- 9 files changed, 39 insertions(+), 8 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index c64da1a8af..c016953c14 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -8,5 +8,7 @@ "react-router-native": "6.12.1", "@remix-run/router": "1.6.3" }, - "changesets": [] + "changesets": [ + "start-transition-minification" + ] } diff --git a/packages/react-router-dom-v5-compat/CHANGELOG.md b/packages/react-router-dom-v5-compat/CHANGELOG.md index 317ff39f54..b18c0880f2 100644 --- a/packages/react-router-dom-v5-compat/CHANGELOG.md +++ b/packages/react-router-dom-v5-compat/CHANGELOG.md @@ -1,5 +1,13 @@ # `react-router-dom-v5-compat` +## 6.12.2-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@6.12.2-pre.0` + - `react-router-dom@6.12.2-pre.0` + ## 6.12.1 ### Patch Changes diff --git a/packages/react-router-dom-v5-compat/package.json b/packages/react-router-dom-v5-compat/package.json index c26069c047..a9df92e30b 100644 --- a/packages/react-router-dom-v5-compat/package.json +++ b/packages/react-router-dom-v5-compat/package.json @@ -1,6 +1,6 @@ { "name": "react-router-dom-v5-compat", - "version": "6.12.1", + "version": "6.12.2-pre.0", "description": "Migration path to React Router v6 from v4/5", "keywords": [ "react", @@ -24,7 +24,7 @@ "types": "./dist/index.d.ts", "dependencies": { "history": "^5.3.0", - "react-router": "6.12.1" + "react-router": "6.12.2-pre.0" }, "peerDependencies": { "react": ">=16.8", diff --git a/packages/react-router-dom/CHANGELOG.md b/packages/react-router-dom/CHANGELOG.md index add1111e10..9b25d397e1 100644 --- a/packages/react-router-dom/CHANGELOG.md +++ b/packages/react-router-dom/CHANGELOG.md @@ -1,5 +1,13 @@ # `react-router-dom` +## 6.12.2-pre.0 + +### Patch Changes + +- Work around webpack/terser `React.startTransition` minification bug in production mode ([`2f79bcef`](https://github.com/remix-run/react-router/commit/2f79bcef5f396943cf95d0d23fbd67df9b8e17a6)) +- Updated dependencies: + - `react-router@6.12.2-pre.0` + ## 6.12.1 ### Patch Changes diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json index 2d397acb21..e3f9f1984b 100644 --- a/packages/react-router-dom/package.json +++ b/packages/react-router-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-router-dom", - "version": "6.12.1", + "version": "6.12.2-pre.0", "description": "Declarative routing for React web applications", "keywords": [ "react", @@ -24,7 +24,7 @@ "types": "./dist/index.d.ts", "dependencies": { "@remix-run/router": "1.6.3", - "react-router": "6.12.1" + "react-router": "6.12.2-pre.0" }, "devDependencies": { "react": "^18.2.0", diff --git a/packages/react-router-native/CHANGELOG.md b/packages/react-router-native/CHANGELOG.md index 01915d3dba..07ef98f26f 100644 --- a/packages/react-router-native/CHANGELOG.md +++ b/packages/react-router-native/CHANGELOG.md @@ -1,5 +1,12 @@ # `react-router-native` +## 6.12.2-pre.0 + +### Patch Changes + +- Updated dependencies: + - `react-router@6.12.2-pre.0` + ## 6.12.1 ### Patch Changes diff --git a/packages/react-router-native/package.json b/packages/react-router-native/package.json index b5ac04265a..8a9c0acf18 100644 --- a/packages/react-router-native/package.json +++ b/packages/react-router-native/package.json @@ -1,6 +1,6 @@ { "name": "react-router-native", - "version": "6.12.1", + "version": "6.12.2-pre.0", "description": "Declarative routing for React Native applications", "keywords": [ "react", @@ -22,7 +22,7 @@ "types": "./dist/index.d.ts", "dependencies": { "@ungap/url-search-params": "^0.1.4", - "react-router": "6.12.1" + "react-router": "6.12.2-pre.0" }, "devDependencies": { "react": "^18.2.0", diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index f1c57ac990..33678babe5 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -1,5 +1,11 @@ # `react-router` +## 6.12.2-pre.0 + +### Patch Changes + +- Work around webpack/terser `React.startTransition` minification bug in production mode ([`2f79bcef`](https://github.com/remix-run/react-router/commit/2f79bcef5f396943cf95d0d23fbd67df9b8e17a6)) + ## 6.12.1 ### Patch Changes diff --git a/packages/react-router/package.json b/packages/react-router/package.json index c8456c45f2..9dccbf0b7d 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "react-router", - "version": "6.12.1", + "version": "6.12.2-pre.0", "description": "Declarative routing for React", "keywords": [ "react", From 5d9be065472dd757a9e877f0710a765a8d4d51bd Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 13 Jun 2023 14:39:00 -0400 Subject: [PATCH 4/9] Add future.v7_startTransition flag (#10596) --- .changeset/v7-start-transition.md | 26 + docs/guides/api-development-strategy.md | 36 +- package.json | 6 +- .../concurrent-mode-navigations-test.tsx | 33 +- .../__tests__/exports-test.tsx | 1 + packages/react-router-dom/index.tsx | 58 +- .../__tests__/exports-test.tsx | 1 + .../react-router/__tests__/navigate-test.tsx | 723 +++++++++++++----- .../__tests__/useNavigate-test.tsx | 1 - packages/react-router/index.ts | 8 +- packages/react-router/lib/components.tsx | 38 +- .../lib/polyfills/start-transition.ts | 28 + 12 files changed, 682 insertions(+), 277 deletions(-) create mode 100644 .changeset/v7-start-transition.md create mode 100644 packages/react-router/lib/polyfills/start-transition.ts diff --git a/.changeset/v7-start-transition.md b/.changeset/v7-start-transition.md new file mode 100644 index 0000000000..22a204b4f5 --- /dev/null +++ b/.changeset/v7-start-transition.md @@ -0,0 +1,26 @@ +--- +"react-router": minor +"react-router-dom": minor +--- + +Move [`React.startTransition`](https://react.dev/reference/react/startTransition) behind a [future flag](https://reactrouter.com/en/main/guides/api-development-strategy) to avoid issues with existing incompatible `Suspense` usages. We recommend folks adopting this flag to be better compatible with React concurrent mode, but if you run into issues you can continue without the use of `startTransition` until v7. Issues usually boils down to creating net-new promises during the render cycle, so if you run into issues you should either lift your promise creation out of the render cycle or put it behind a `useMemo`. + +Existing behavior will no longer include `React.startTransition`: + +```jsx + + {/*...*/} + + + +``` + +If you wish to enable `React.startTransition`, pass the future flag to your component: + +```jsx + + {/*...*/} + + + +``` diff --git a/docs/guides/api-development-strategy.md b/docs/guides/api-development-strategy.md index 51655a7156..aca10bc852 100644 --- a/docs/guides/api-development-strategy.md +++ b/docs/guides/api-development-strategy.md @@ -49,12 +49,46 @@ The lifecycle is thus either: ## Current Future Flags -Here's the current future flags in React Router v6 today: +Here's the current future flags in React Router v6 today. + +### `@remix-run/router` Future Flags + +These flags are only applicable when using a [Data Router][picking-a-router] and are passed when creating the `router` instance: + +```js +const router = createBrowserRouter(routes, { + future: { + v7_normalizeFormMethod: true, + }, +}); +``` | Flag | Description | | ------------------------ | --------------------------------------------------------------------- | | `v7_normalizeFormMethod` | Normalize `useNavigation().formMethod` to be an uppercase HTTP Method | +### React Router Future Flags + +These flags apply to both Data and non-Data Routers and are passed to the rendered React component: + +```jsx + + {/*...*/} + +``` + +```jsx + +``` + +| Flag | Description | +| -------------------- | --------------------------------------------------------------------------- | +| `v7_startTransition` | Wrap all router state updates in [`React.startTransition`][starttransition] | + [future-flags-blog-post]: https://remix.run/blog/future-flags [feature-flowchart]: https://remix.run/docs-images/feature-flowchart.png [picking-a-router]: ../routers/picking-a-router +[starttransition]: https://react.dev/reference/react/startTransition diff --git a/package.json b/package.json index 18618426b5..6c75c12ad1 100644 --- a/package.json +++ b/package.json @@ -112,16 +112,16 @@ "none": "45 kB" }, "packages/react-router/dist/react-router.production.min.js": { - "none": "13.4 kB" + "none": "13.5 kB" }, "packages/react-router/dist/umd/react-router.production.min.js": { "none": "15.8 kB" }, "packages/react-router-dom/dist/react-router-dom.production.min.js": { - "none": "12.0 kB" + "none": "12.1 kB" }, "packages/react-router-dom/dist/umd/react-router-dom.production.min.js": { - "none": "18.0 kB" + "none": "18.1 kB" } } } diff --git a/packages/react-router-dom/__tests__/concurrent-mode-navigations-test.tsx b/packages/react-router-dom/__tests__/concurrent-mode-navigations-test.tsx index 6e7839a012..5f494ffcd0 100644 --- a/packages/react-router-dom/__tests__/concurrent-mode-navigations-test.tsx +++ b/packages/react-router-dom/__tests__/concurrent-mode-navigations-test.tsx @@ -19,7 +19,6 @@ import { waitFor, } from "@testing-library/react"; import { JSDOM } from "jsdom"; -import LazyComponent from "./components//LazyComponent"; describe("Handles concurrent mode features during navigations", () => { function getComponents() { @@ -117,7 +116,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); }); @@ -288,7 +295,7 @@ describe("Handles concurrent mode features during navigations", () => { getComponents(); let { container } = render( - + } /> } /> @@ -306,7 +313,10 @@ describe("Handles concurrent mode features during navigations", () => { getComponents(); let { container } = render( - + } /> } /> @@ -324,7 +334,10 @@ describe("Handles concurrent mode features during navigations", () => { getComponents(); let { container } = render( - + } /> } /> @@ -350,7 +363,9 @@ describe("Handles concurrent mode features during navigations", () => { ) ); - let { container } = render(); + let { container } = render( + + ); await assertNavigation(container, resolve, resolveLazy); }); diff --git a/packages/react-router-dom/__tests__/exports-test.tsx b/packages/react-router-dom/__tests__/exports-test.tsx index e3b479d6f1..7e87c75a81 100644 --- a/packages/react-router-dom/__tests__/exports-test.tsx +++ b/packages/react-router-dom/__tests__/exports-test.tsx @@ -4,6 +4,7 @@ import * as ReactRouterDOM from "react-router-dom"; let nonReExportedKeys = new Set([ "UNSAFE_mapRouteProperties", "UNSAFE_useRoutesImpl", + "UNSAFE_startTransitionImpl", ]); describe("react-router-dom", () => { diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index d2356a84f0..98bc1f7e8c 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -4,6 +4,7 @@ */ import * as React from "react"; import type { + FutureConfig, Location, NavigateOptions, NavigationType, @@ -26,6 +27,7 @@ import { UNSAFE_NavigationContext as NavigationContext, UNSAFE_RouteContext as RouteContext, UNSAFE_mapRouteProperties as mapRouteProperties, + UNSAFE_startTransitionImpl as startTransitionImpl, UNSAFE_useRouteId as useRouteId, } from "react-router"; import type { @@ -33,7 +35,7 @@ import type { Fetcher, FormEncType, FormMethod, - FutureConfig, + FutureConfig as RouterFutureConfig, GetScrollRestorationKeyFunction, HashHistory, History, @@ -209,7 +211,7 @@ declare global { interface DOMRouterOpts { basename?: string; - future?: Partial>; + future?: Partial>; hydrationData?: HydrationState; window?: Window; } @@ -297,34 +299,17 @@ function deserializeErrors( export interface BrowserRouterProps { basename?: string; children?: React.ReactNode; + future?: FutureConfig; window?: Window; } -// Webpack + React 17 fails to compile on any of the following: -// * 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 reference once up front resolves that issue. -const START_TRANSITION = "startTransition"; -const startTransitionImpl = React[START_TRANSITION]; - /** * A `` for use in web browsers. Provides the cleanest URLs. */ export function BrowserRouter({ basename, children, + future, window, }: BrowserRouterProps) { let historyRef = React.useRef(); @@ -337,13 +322,14 @@ export function BrowserRouter({ action: history.action, location: history.location, }); + let { v7_startTransition } = future || {}; let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - startTransitionImpl + v7_startTransition && startTransitionImpl ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); }, - [setStateImpl] + [setStateImpl, v7_startTransition] ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -362,6 +348,7 @@ export function BrowserRouter({ export interface HashRouterProps { basename?: string; children?: React.ReactNode; + future?: FutureConfig; window?: Window; } @@ -369,7 +356,12 @@ export interface HashRouterProps { * A `` for use in web browsers. Stores the location in the hash * portion of the URL so it is not sent to the server. */ -export function HashRouter({ basename, children, window }: HashRouterProps) { +export function HashRouter({ + basename, + children, + future, + window, +}: HashRouterProps) { let historyRef = React.useRef(); if (historyRef.current == null) { historyRef.current = createHashHistory({ window, v5Compat: true }); @@ -380,13 +372,14 @@ export function HashRouter({ basename, children, window }: HashRouterProps) { action: history.action, location: history.location, }); + let { v7_startTransition } = future || {}; let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - startTransitionImpl + v7_startTransition && startTransitionImpl ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); }, - [setStateImpl] + [setStateImpl, v7_startTransition] ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -405,6 +398,7 @@ export function HashRouter({ basename, children, window }: HashRouterProps) { export interface HistoryRouterProps { basename?: string; children?: React.ReactNode; + future?: FutureConfig; history: History; } @@ -414,18 +408,24 @@ export interface HistoryRouterProps { * two versions of the history library to your bundles unless you use the same * version of the history library that React Router uses internally. */ -function HistoryRouter({ basename, children, history }: HistoryRouterProps) { +function HistoryRouter({ + basename, + children, + future, + 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 }) => { - startTransitionImpl + v7_startTransition && startTransitionImpl ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); }, - [setStateImpl] + [setStateImpl, v7_startTransition] ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); diff --git a/packages/react-router-native/__tests__/exports-test.tsx b/packages/react-router-native/__tests__/exports-test.tsx index a73c1e42ba..ab49f423d6 100644 --- a/packages/react-router-native/__tests__/exports-test.tsx +++ b/packages/react-router-native/__tests__/exports-test.tsx @@ -4,6 +4,7 @@ import * as ReactRouterNative from "react-router-native"; let nonReExportedKeys = new Set([ "UNSAFE_mapRouteProperties", "UNSAFE_useRoutesImpl", + "UNSAFE_startTransitionImpl", ]); describe("react-router-native", () => { diff --git a/packages/react-router/__tests__/navigate-test.tsx b/packages/react-router/__tests__/navigate-test.tsx index 7c12862284..8a3c03615f 100644 --- a/packages/react-router/__tests__/navigate-test.tsx +++ b/packages/react-router/__tests__/navigate-test.tsx @@ -564,51 +564,54 @@ describe("", () => { " `); }); +}); - 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", () => { + 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 ; + }, }, - }, - { - 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 @@ -618,64 +621,133 @@ describe("", () => {

" `); - 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]); - }); + 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(() => { + 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]); + }); + + 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); } - }, [count]); - return ; + return ; + }, }, - }, - { - path: "b", - Component() { - let { state } = useLocation() as { state: { count: number } }; - renders.push(state.count); - return ( - <> -

Page B

-

{state.count}

- - ); + { + 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 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 @@ -685,66 +757,219 @@ describe("", () => {

" `); - 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(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 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; + }, + 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(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]); + }); }); - 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 ; + 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", - 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}

+ + ); + }, + }, + ], + }, + ]); + + 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 ; + }, }, - 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 @@ -754,72 +979,151 @@ describe("", () => {

" `); - 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]); - }); + 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]); + }); - 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(() => { + 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); } - }, [count]); - return ; + 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 { 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; + }, + 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 @@ -829,23 +1133,24 @@ describe("", () => {

" `); - 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__/useNavigate-test.tsx b/packages/react-router/__tests__/useNavigate-test.tsx index 4512c03c5f..95499722d5 100644 --- a/packages/react-router/__tests__/useNavigate-test.tsx +++ b/packages/react-router/__tests__/useNavigate-test.tsx @@ -7,7 +7,6 @@ import { Route, useNavigate, useLocation, - useRoutes, createMemoryRouter, createRoutesFromElements, Outlet, diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 8167fcea9c..ad1e1fd56e 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -23,7 +23,7 @@ import type { To, InitialEntry, LazyRouteFunction, - FutureConfig, + FutureConfig as RouterFutureConfig, } from "@remix-run/router"; import { AbortedDeferredError, @@ -43,6 +43,7 @@ import { UNSAFE_warning as warning, } from "@remix-run/router"; +import startTransitionImpl from "./lib/polyfills/start-transition"; import type { AwaitProps, MemoryRouterProps, @@ -55,6 +56,7 @@ import type { RouterProps, RoutesProps, RouterProviderProps, + FutureConfig, } from "./lib/components"; import { createRoutesFromChildren, @@ -127,6 +129,7 @@ export type { DataRouteMatch, DataRouteObject, Fetcher, + FutureConfig, Hash, IndexRouteObject, IndexRouteProps, @@ -256,7 +259,7 @@ export function createMemoryRouter( routes: RouteObject[], opts?: { basename?: string; - future?: Partial>; + future?: Partial>; hydrationData?: HydrationState; initialEntries?: InitialEntry[]; initialIndex?: number; @@ -301,4 +304,5 @@ export { mapRouteProperties as UNSAFE_mapRouteProperties, useRouteId as UNSAFE_useRouteId, useRoutesImpl as UNSAFE_useRoutesImpl, + startTransitionImpl as UNSAFE_startTransitionImpl, }; diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index e4d4eee360..444b496bf3 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -22,6 +22,7 @@ import { UNSAFE_getPathContributingMatches as getPathContributingMatches, } from "@remix-run/router"; +import startTransitionImpl from "./polyfills/start-transition"; import type { DataRouteObject, IndexRouteObject, @@ -49,47 +50,35 @@ import { useLocation, } from "./hooks"; +export interface FutureConfig { + v7_startTransition: boolean; +} + export interface RouterProviderProps { fallbackElement?: React.ReactNode; router: RemixRouter; + future?: FutureConfig; } -// Webpack + React 17 fails to compile on any of the following: -// * 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 reference once up front resolves that issue. -const START_TRANSITION = "startTransition"; -const startTransitionImpl = React[START_TRANSITION]; - /** * Given a Remix Router instance, render the appropriate UI */ export function RouterProvider({ fallbackElement, router, + future, }: RouterProviderProps): React.ReactElement { // Need to use a layout effect here so we are subscribed early enough to // pick up on any render-driven redirects/navigations (useEffect/) let [state, setStateImpl] = React.useState(router.state); + let { v7_startTransition } = future || {}; let setState = React.useCallback( (newState: RouterState) => { - startTransitionImpl + v7_startTransition && startTransitionImpl ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); }, - [setStateImpl] + [setStateImpl, v7_startTransition] ); React.useLayoutEffect(() => router.subscribe(setState), [router, setState]); @@ -168,6 +157,7 @@ export interface MemoryRouterProps { children?: React.ReactNode; initialEntries?: InitialEntry[]; initialIndex?: number; + future?: FutureConfig; } /** @@ -180,6 +170,7 @@ export function MemoryRouter({ children, initialEntries, initialIndex, + future, }: MemoryRouterProps): React.ReactElement { let historyRef = React.useRef(); if (historyRef.current == null) { @@ -195,13 +186,14 @@ export function MemoryRouter({ action: history.action, location: history.location, }); + let { v7_startTransition } = future || {}; let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - startTransitionImpl + v7_startTransition && startTransitionImpl ? startTransitionImpl(() => setStateImpl(newState)) : setStateImpl(newState); }, - [setStateImpl] + [setStateImpl, v7_startTransition] ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); diff --git a/packages/react-router/lib/polyfills/start-transition.ts b/packages/react-router/lib/polyfills/start-transition.ts new file mode 100644 index 0000000000..72c6b8bba3 --- /dev/null +++ b/packages/react-router/lib/polyfills/start-transition.ts @@ -0,0 +1,28 @@ +import * as React from "react"; + +/** + Not a true "polyfill" since we guard via the feature flag at runtime, + but close enough :) + + 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"; +export default React[START_TRANSITION]; From a0b53e36fb9f9c408cb36a3d69959d4703cda994 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 14:41:13 -0400 Subject: [PATCH 5/9] chore: Update version for release (pre) (#10597) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 3 +- .../react-router-dom-v5-compat/CHANGELOG.md | 8 +++++ .../react-router-dom-v5-compat/package.json | 4 +-- packages/react-router-dom/CHANGELOG.md | 31 +++++++++++++++++++ packages/react-router-dom/package.json | 4 +-- packages/react-router-native/CHANGELOG.md | 7 +++++ packages/react-router-native/package.json | 4 +-- packages/react-router/CHANGELOG.md | 26 ++++++++++++++++ packages/react-router/package.json | 2 +- 9 files changed, 81 insertions(+), 8 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index c016953c14..20539b2f49 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -9,6 +9,7 @@ "@remix-run/router": "1.6.3" }, "changesets": [ - "start-transition-minification" + "start-transition-minification", + "v7-start-transition" ] } diff --git a/packages/react-router-dom-v5-compat/CHANGELOG.md b/packages/react-router-dom-v5-compat/CHANGELOG.md index b18c0880f2..eff97a47cc 100644 --- a/packages/react-router-dom-v5-compat/CHANGELOG.md +++ b/packages/react-router-dom-v5-compat/CHANGELOG.md @@ -1,5 +1,13 @@ # `react-router-dom-v5-compat` +## 6.13.0-pre.1 + +### Patch Changes + +- Updated dependencies: + - `react-router@6.13.0-pre.1` + - `react-router-dom@6.13.0-pre.1` + ## 6.12.2-pre.0 ### Patch Changes diff --git a/packages/react-router-dom-v5-compat/package.json b/packages/react-router-dom-v5-compat/package.json index a9df92e30b..6119777266 100644 --- a/packages/react-router-dom-v5-compat/package.json +++ b/packages/react-router-dom-v5-compat/package.json @@ -1,6 +1,6 @@ { "name": "react-router-dom-v5-compat", - "version": "6.12.2-pre.0", + "version": "6.13.0-pre.1", "description": "Migration path to React Router v6 from v4/5", "keywords": [ "react", @@ -24,7 +24,7 @@ "types": "./dist/index.d.ts", "dependencies": { "history": "^5.3.0", - "react-router": "6.12.2-pre.0" + "react-router": "6.13.0-pre.1" }, "peerDependencies": { "react": ">=16.8", diff --git a/packages/react-router-dom/CHANGELOG.md b/packages/react-router-dom/CHANGELOG.md index 9b25d397e1..59cb240210 100644 --- a/packages/react-router-dom/CHANGELOG.md +++ b/packages/react-router-dom/CHANGELOG.md @@ -1,5 +1,36 @@ # `react-router-dom` +## 6.13.0-pre.1 + +### Minor Changes + +- Move [`React.startTransition`](https://react.dev/reference/react/startTransition) behind a [future flag](https://reactrouter.com/en/main/guides/api-development-strategy) to avoid issues with existing incompatible `Suspense` usages. We recommend folks adopting this flag to be better compatible with React concurrent mode, but if you run into issues you can continue without the use of `startTransition` until v7. Issues usually boils down to creating net-new promises during the render cycle, so if you run into issues you should either lift your promise creation out of the render cycle or put it behind a `useMemo`. ([#10596](https://github.com/remix-run/react-router/pull/10596)) + + Existing behavior will no longer include `React.startTransition`: + + ```jsx + + {/*...*/} + + + + ``` + + If you wish to enable `React.startTransition`, pass the future flag to your component: + + ```jsx + + {/*...*/} + + + + ``` + +### Patch Changes + +- Updated dependencies: + - `react-router@6.13.0-pre.1` + ## 6.12.2-pre.0 ### Patch Changes diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json index e3f9f1984b..b84c42025f 100644 --- a/packages/react-router-dom/package.json +++ b/packages/react-router-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-router-dom", - "version": "6.12.2-pre.0", + "version": "6.13.0-pre.1", "description": "Declarative routing for React web applications", "keywords": [ "react", @@ -24,7 +24,7 @@ "types": "./dist/index.d.ts", "dependencies": { "@remix-run/router": "1.6.3", - "react-router": "6.12.2-pre.0" + "react-router": "6.13.0-pre.1" }, "devDependencies": { "react": "^18.2.0", diff --git a/packages/react-router-native/CHANGELOG.md b/packages/react-router-native/CHANGELOG.md index 07ef98f26f..bc4bff18e0 100644 --- a/packages/react-router-native/CHANGELOG.md +++ b/packages/react-router-native/CHANGELOG.md @@ -1,5 +1,12 @@ # `react-router-native` +## 6.13.0-pre.1 + +### Patch Changes + +- Updated dependencies: + - `react-router@6.13.0-pre.1` + ## 6.12.2-pre.0 ### Patch Changes diff --git a/packages/react-router-native/package.json b/packages/react-router-native/package.json index 8a9c0acf18..2e1b65d2fe 100644 --- a/packages/react-router-native/package.json +++ b/packages/react-router-native/package.json @@ -1,6 +1,6 @@ { "name": "react-router-native", - "version": "6.12.2-pre.0", + "version": "6.13.0-pre.1", "description": "Declarative routing for React Native applications", "keywords": [ "react", @@ -22,7 +22,7 @@ "types": "./dist/index.d.ts", "dependencies": { "@ungap/url-search-params": "^0.1.4", - "react-router": "6.12.2-pre.0" + "react-router": "6.13.0-pre.1" }, "devDependencies": { "react": "^18.2.0", diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index 33678babe5..fb2ad8f611 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -1,5 +1,31 @@ # `react-router` +## 6.13.0-pre.1 + +### Minor Changes + +- Move [`React.startTransition`](https://react.dev/reference/react/startTransition) behind a [future flag](https://reactrouter.com/en/main/guides/api-development-strategy) to avoid issues with existing incompatible `Suspense` usages. We recommend folks adopting this flag to be better compatible with React concurrent mode, but if you run into issues you can continue without the use of `startTransition` until v7. Issues usually boils down to creating net-new promises during the render cycle, so if you run into issues you should either lift your promise creation out of the render cycle or put it behind a `useMemo`. ([#10596](https://github.com/remix-run/react-router/pull/10596)) + + Existing behavior will no longer include `React.startTransition`: + + ```jsx + + {/*...*/} + + + + ``` + + If you wish to enable `React.startTransition`, pass the future flag to your component: + + ```jsx + + {/*...*/} + + + + ``` + ## 6.12.2-pre.0 ### Patch Changes diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 9dccbf0b7d..c605aac09b 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "react-router", - "version": "6.12.2-pre.0", + "version": "6.13.0-pre.1", "description": "Declarative routing for React", "keywords": [ "react", From 70737950ab13ff47d39a83afd7fd48d179bffdcd Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 14 Jun 2023 11:29:33 -0400 Subject: [PATCH 6/9] Update docs for future flags --- docs/router-components/browser-router.md | 38 ++++++++++++++++++++++-- docs/router-components/hash-router.md | 38 ++++++++++++++++++++++-- docs/router-components/memory-router.md | 33 ++++++++++++++++++++ docs/routers/router-provider.md | 33 ++++++++++++++++++++ 4 files changed, 138 insertions(+), 4 deletions(-) diff --git a/docs/router-components/browser-router.md b/docs/router-components/browser-router.md index a34f774ed1..c46c49e616 100644 --- a/docs/router-components/browser-router.md +++ b/docs/router-components/browser-router.md @@ -15,6 +15,7 @@ declare function BrowserRouter( interface BrowserRouterProps { basename?: string; children?: React.ReactNode; + future?: FutureConfig; window?: Window; } ``` @@ -23,8 +24,6 @@ interface BrowserRouterProps { A `` stores the current location in the browser's address bar using clean URLs and navigates using the browser's built-in history stack. -`` defaults to using the current [document's `defaultView`][defaultview], but it may also be used to track changes to another window's URL, in an `