Skip to content

Commit 5d9be06

Browse files
authored
Add future.v7_startTransition flag (#10596)
1 parent 31bdd23 commit 5d9be06

File tree

12 files changed

+682
-277
lines changed

12 files changed

+682
-277
lines changed

.changeset/v7-start-transition.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"react-router": minor
3+
"react-router-dom": minor
4+
---
5+
6+
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`.
7+
8+
Existing behavior will no longer include `React.startTransition`:
9+
10+
```jsx
11+
<BrowserRouter>
12+
<Routes>{/*...*/}</Routes>
13+
</BrowserRouter>
14+
15+
<RouterProvider router={router} />
16+
```
17+
18+
If you wish to enable `React.startTransition`, pass the future flag to your component:
19+
20+
```jsx
21+
<BrowserRouter future={{ v7_startTransition: true }}>
22+
<Routes>{/*...*/}</Routes>
23+
</BrowserRouter>
24+
25+
<RouterProvider router={router} future={{ v7_startTransition: true }}/>
26+
```

docs/guides/api-development-strategy.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,46 @@ The lifecycle is thus either:
4949

5050
## Current Future Flags
5151

52-
Here's the current future flags in React Router v6 today:
52+
Here's the current future flags in React Router v6 today.
53+
54+
### `@remix-run/router` Future Flags
55+
56+
These flags are only applicable when using a [Data Router][picking-a-router] and are passed when creating the `router` instance:
57+
58+
```js
59+
const router = createBrowserRouter(routes, {
60+
future: {
61+
v7_normalizeFormMethod: true,
62+
},
63+
});
64+
```
5365

5466
| Flag | Description |
5567
| ------------------------ | --------------------------------------------------------------------- |
5668
| `v7_normalizeFormMethod` | Normalize `useNavigation().formMethod` to be an uppercase HTTP Method |
5769

70+
### React Router Future Flags
71+
72+
These flags apply to both Data and non-Data Routers and are passed to the rendered React component:
73+
74+
```jsx
75+
<BrowserRouter future={{ v7_normalizeFormMethod: true }}>
76+
<Routes>{/*...*/}</Routes>
77+
</BrowserRouter>
78+
```
79+
80+
```jsx
81+
<RouterProvider
82+
router={router}
83+
future={{ v7_normalizeFormMethod: true }}
84+
/>
85+
```
86+
87+
| Flag | Description |
88+
| -------------------- | --------------------------------------------------------------------------- |
89+
| `v7_startTransition` | Wrap all router state updates in [`React.startTransition`][starttransition] |
90+
5891
[future-flags-blog-post]: https://remix.run/blog/future-flags
5992
[feature-flowchart]: https://remix.run/docs-images/feature-flowchart.png
6093
[picking-a-router]: ../routers/picking-a-router
94+
[starttransition]: https://react.dev/reference/react/startTransition

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,16 @@
112112
"none": "45 kB"
113113
},
114114
"packages/react-router/dist/react-router.production.min.js": {
115-
"none": "13.4 kB"
115+
"none": "13.5 kB"
116116
},
117117
"packages/react-router/dist/umd/react-router.production.min.js": {
118118
"none": "15.8 kB"
119119
},
120120
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
121-
"none": "12.0 kB"
121+
"none": "12.1 kB"
122122
},
123123
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
124-
"none": "18.0 kB"
124+
"none": "18.1 kB"
125125
}
126126
}
127127
}

packages/react-router-dom/__tests__/concurrent-mode-navigations-test.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
waitFor,
2020
} from "@testing-library/react";
2121
import { JSDOM } from "jsdom";
22-
import LazyComponent from "./components//LazyComponent";
2322

2423
describe("Handles concurrent mode features during navigations", () => {
2524
function getComponents() {
@@ -117,7 +116,7 @@ describe("Handles concurrent mode features during navigations", () => {
117116
getComponents();
118117

119118
let { container } = render(
120-
<MemoryRouter>
119+
<MemoryRouter future={{ v7_startTransition: true }}>
121120
<Routes>
122121
<Route path="/" element={<Home />} />
123122
<Route
@@ -149,7 +148,10 @@ describe("Handles concurrent mode features during navigations", () => {
149148
getComponents();
150149

151150
let { container } = render(
152-
<BrowserRouter window={getWindowImpl("/", false)}>
151+
<BrowserRouter
152+
window={getWindowImpl("/", false)}
153+
future={{ v7_startTransition: true }}
154+
>
153155
<Routes>
154156
<Route path="/" element={<Home />} />
155157
<Route
@@ -181,7 +183,10 @@ describe("Handles concurrent mode features during navigations", () => {
181183
getComponents();
182184

183185
let { container } = render(
184-
<HashRouter window={getWindowImpl("/", true)}>
186+
<HashRouter
187+
window={getWindowImpl("/", true)}
188+
future={{ v7_startTransition: true }}
189+
>
185190
<Routes>
186191
<Route path="/" element={<Home />} />
187192
<Route
@@ -235,7 +240,9 @@ describe("Handles concurrent mode features during navigations", () => {
235240
</>
236241
)
237242
);
238-
let { container } = render(<RouterProvider router={router} />);
243+
let { container } = render(
244+
<RouterProvider router={router} future={{ v7_startTransition: true }} />
245+
);
239246

240247
await assertNavigation(container, resolve, resolveLazy);
241248
});
@@ -288,7 +295,7 @@ describe("Handles concurrent mode features during navigations", () => {
288295
getComponents();
289296

290297
let { container } = render(
291-
<MemoryRouter>
298+
<MemoryRouter future={{ v7_startTransition: true }}>
292299
<Routes>
293300
<Route path="/" element={<Home />} />
294301
<Route path="/about" element={<About />} />
@@ -306,7 +313,10 @@ describe("Handles concurrent mode features during navigations", () => {
306313
getComponents();
307314

308315
let { container } = render(
309-
<BrowserRouter window-={getWindowImpl("/", true)}>
316+
<BrowserRouter
317+
window-={getWindowImpl("/", true)}
318+
future={{ v7_startTransition: true }}
319+
>
310320
<Routes>
311321
<Route path="/" element={<Home />} />
312322
<Route path="/about" element={<About />} />
@@ -324,7 +334,10 @@ describe("Handles concurrent mode features during navigations", () => {
324334
getComponents();
325335

326336
let { container } = render(
327-
<HashRouter window-={getWindowImpl("/", true)}>
337+
<HashRouter
338+
window-={getWindowImpl("/", true)}
339+
future={{ v7_startTransition: true }}
340+
>
328341
<Routes>
329342
<Route path="/" element={<Home />} />
330343
<Route path="/about" element={<About />} />
@@ -350,7 +363,9 @@ describe("Handles concurrent mode features during navigations", () => {
350363
</>
351364
)
352365
);
353-
let { container } = render(<RouterProvider router={router} />);
366+
let { container } = render(
367+
<RouterProvider router={router} future={{ v7_startTransition: true }} />
368+
);
354369

355370
await assertNavigation(container, resolve, resolveLazy);
356371
});

packages/react-router-dom/__tests__/exports-test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ReactRouterDOM from "react-router-dom";
44
let nonReExportedKeys = new Set([
55
"UNSAFE_mapRouteProperties",
66
"UNSAFE_useRoutesImpl",
7+
"UNSAFE_startTransitionImpl",
78
]);
89

910
describe("react-router-dom", () => {

packages/react-router-dom/index.tsx

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
import * as React from "react";
66
import type {
7+
FutureConfig,
78
Location,
89
NavigateOptions,
910
NavigationType,
@@ -26,14 +27,15 @@ import {
2627
UNSAFE_NavigationContext as NavigationContext,
2728
UNSAFE_RouteContext as RouteContext,
2829
UNSAFE_mapRouteProperties as mapRouteProperties,
30+
UNSAFE_startTransitionImpl as startTransitionImpl,
2931
UNSAFE_useRouteId as useRouteId,
3032
} from "react-router";
3133
import type {
3234
BrowserHistory,
3335
Fetcher,
3436
FormEncType,
3537
FormMethod,
36-
FutureConfig,
38+
FutureConfig as RouterFutureConfig,
3739
GetScrollRestorationKeyFunction,
3840
HashHistory,
3941
History,
@@ -209,7 +211,7 @@ declare global {
209211

210212
interface DOMRouterOpts {
211213
basename?: string;
212-
future?: Partial<Omit<FutureConfig, "v7_prependBasename">>;
214+
future?: Partial<Omit<RouterFutureConfig, "v7_prependBasename">>;
213215
hydrationData?: HydrationState;
214216
window?: Window;
215217
}
@@ -297,34 +299,17 @@ function deserializeErrors(
297299
export interface BrowserRouterProps {
298300
basename?: string;
299301
children?: React.ReactNode;
302+
future?: FutureConfig;
300303
window?: Window;
301304
}
302305

303-
// Webpack + React 17 fails to compile on any of the following:
304-
// * import { startTransition } from "react"
305-
// * import * as React from from "react";
306-
// "startTransition" in React ? React.startTransition(() => setState()) : setState()
307-
// * import * as React from from "react";
308-
// "startTransition" in React ? React["startTransition"](() => setState()) : setState()
309-
//
310-
// Moving it to a constant such as the following solves the Webpack/React 17 issue:
311-
// * import * as React from from "react";
312-
// const START_TRANSITION = "startTransition";
313-
// START_TRANSITION in React ? React[START_TRANSITION](() => setState()) : setState()
314-
//
315-
// However, that introduces webpack/terser minification issues in production builds
316-
// in React 18 where minification/obfuscation ends up removing the call of
317-
// React.startTransition entirely from the first half of the ternary. Grabbing
318-
// this reference once up front resolves that issue.
319-
const START_TRANSITION = "startTransition";
320-
const startTransitionImpl = React[START_TRANSITION];
321-
322306
/**
323307
* A `<Router>` for use in web browsers. Provides the cleanest URLs.
324308
*/
325309
export function BrowserRouter({
326310
basename,
327311
children,
312+
future,
328313
window,
329314
}: BrowserRouterProps) {
330315
let historyRef = React.useRef<BrowserHistory>();
@@ -337,13 +322,14 @@ export function BrowserRouter({
337322
action: history.action,
338323
location: history.location,
339324
});
325+
let { v7_startTransition } = future || {};
340326
let setState = React.useCallback(
341327
(newState: { action: NavigationType; location: Location }) => {
342-
startTransitionImpl
328+
v7_startTransition && startTransitionImpl
343329
? startTransitionImpl(() => setStateImpl(newState))
344330
: setStateImpl(newState);
345331
},
346-
[setStateImpl]
332+
[setStateImpl, v7_startTransition]
347333
);
348334

349335
React.useLayoutEffect(() => history.listen(setState), [history, setState]);
@@ -362,14 +348,20 @@ export function BrowserRouter({
362348
export interface HashRouterProps {
363349
basename?: string;
364350
children?: React.ReactNode;
351+
future?: FutureConfig;
365352
window?: Window;
366353
}
367354

368355
/**
369356
* A `<Router>` for use in web browsers. Stores the location in the hash
370357
* portion of the URL so it is not sent to the server.
371358
*/
372-
export function HashRouter({ basename, children, window }: HashRouterProps) {
359+
export function HashRouter({
360+
basename,
361+
children,
362+
future,
363+
window,
364+
}: HashRouterProps) {
373365
let historyRef = React.useRef<HashHistory>();
374366
if (historyRef.current == null) {
375367
historyRef.current = createHashHistory({ window, v5Compat: true });
@@ -380,13 +372,14 @@ export function HashRouter({ basename, children, window }: HashRouterProps) {
380372
action: history.action,
381373
location: history.location,
382374
});
375+
let { v7_startTransition } = future || {};
383376
let setState = React.useCallback(
384377
(newState: { action: NavigationType; location: Location }) => {
385-
startTransitionImpl
378+
v7_startTransition && startTransitionImpl
386379
? startTransitionImpl(() => setStateImpl(newState))
387380
: setStateImpl(newState);
388381
},
389-
[setStateImpl]
382+
[setStateImpl, v7_startTransition]
390383
);
391384

392385
React.useLayoutEffect(() => history.listen(setState), [history, setState]);
@@ -405,6 +398,7 @@ export function HashRouter({ basename, children, window }: HashRouterProps) {
405398
export interface HistoryRouterProps {
406399
basename?: string;
407400
children?: React.ReactNode;
401+
future?: FutureConfig;
408402
history: History;
409403
}
410404

@@ -414,18 +408,24 @@ export interface HistoryRouterProps {
414408
* two versions of the history library to your bundles unless you use the same
415409
* version of the history library that React Router uses internally.
416410
*/
417-
function HistoryRouter({ basename, children, history }: HistoryRouterProps) {
411+
function HistoryRouter({
412+
basename,
413+
children,
414+
future,
415+
history,
416+
}: HistoryRouterProps) {
418417
let [state, setStateImpl] = React.useState({
419418
action: history.action,
420419
location: history.location,
421420
});
421+
let { v7_startTransition } = future || {};
422422
let setState = React.useCallback(
423423
(newState: { action: NavigationType; location: Location }) => {
424-
startTransitionImpl
424+
v7_startTransition && startTransitionImpl
425425
? startTransitionImpl(() => setStateImpl(newState))
426426
: setStateImpl(newState);
427427
},
428-
[setStateImpl]
428+
[setStateImpl, v7_startTransition]
429429
);
430430

431431
React.useLayoutEffect(() => history.listen(setState), [history, setState]);

packages/react-router-native/__tests__/exports-test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ReactRouterNative from "react-router-native";
44
let nonReExportedKeys = new Set([
55
"UNSAFE_mapRouteProperties",
66
"UNSAFE_useRoutesImpl",
7+
"UNSAFE_startTransitionImpl",
78
]);
89

910
describe("react-router-native", () => {

0 commit comments

Comments
 (0)