Skip to content

Commit

Permalink
Add future.v7_startTransition flag (#10596)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Jun 13, 2023
1 parent 31bdd23 commit 5d9be06
Show file tree
Hide file tree
Showing 12 changed files with 682 additions and 277 deletions.
26 changes: 26 additions & 0 deletions .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
<BrowserRouter>
<Routes>{/*...*/}</Routes>
</BrowserRouter>

<RouterProvider router={router} />
```

If you wish to enable `React.startTransition`, pass the future flag to your component:

```jsx
<BrowserRouter future={{ v7_startTransition: true }}>
<Routes>{/*...*/}</Routes>
</BrowserRouter>

<RouterProvider router={router} future={{ v7_startTransition: true }}/>
```
36 changes: 35 additions & 1 deletion docs/guides/api-development-strategy.md
Expand Up @@ -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
<BrowserRouter future={{ v7_normalizeFormMethod: true }}>
<Routes>{/*...*/}</Routes>
</BrowserRouter>
```

```jsx
<RouterProvider
router={router}
future={{ v7_normalizeFormMethod: true }}
/>
```

| 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
6 changes: 3 additions & 3 deletions package.json
Expand Up @@ -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"
}
}
}
Expand Up @@ -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() {
Expand Down Expand Up @@ -117,7 +116,7 @@ describe("Handles concurrent mode features during navigations", () => {
getComponents();

let { container } = render(
<MemoryRouter>
<MemoryRouter future={{ v7_startTransition: true }}>
<Routes>
<Route path="/" element={<Home />} />
<Route
Expand Down Expand Up @@ -149,7 +148,10 @@ describe("Handles concurrent mode features during navigations", () => {
getComponents();

let { container } = render(
<BrowserRouter window={getWindowImpl("/", false)}>
<BrowserRouter
window={getWindowImpl("/", false)}
future={{ v7_startTransition: true }}
>
<Routes>
<Route path="/" element={<Home />} />
<Route
Expand Down Expand Up @@ -181,7 +183,10 @@ describe("Handles concurrent mode features during navigations", () => {
getComponents();

let { container } = render(
<HashRouter window={getWindowImpl("/", true)}>
<HashRouter
window={getWindowImpl("/", true)}
future={{ v7_startTransition: true }}
>
<Routes>
<Route path="/" element={<Home />} />
<Route
Expand Down Expand Up @@ -235,7 +240,9 @@ describe("Handles concurrent mode features during navigations", () => {
</>
)
);
let { container } = render(<RouterProvider router={router} />);
let { container } = render(
<RouterProvider router={router} future={{ v7_startTransition: true }} />
);

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

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

let { container } = render(
<BrowserRouter window-={getWindowImpl("/", true)}>
<BrowserRouter
window-={getWindowImpl("/", true)}
future={{ v7_startTransition: true }}
>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
Expand All @@ -324,7 +334,10 @@ describe("Handles concurrent mode features during navigations", () => {
getComponents();

let { container } = render(
<HashRouter window-={getWindowImpl("/", true)}>
<HashRouter
window-={getWindowImpl("/", true)}
future={{ v7_startTransition: true }}
>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
Expand All @@ -350,7 +363,9 @@ describe("Handles concurrent mode features during navigations", () => {
</>
)
);
let { container } = render(<RouterProvider router={router} />);
let { container } = render(
<RouterProvider router={router} future={{ v7_startTransition: true }} />
);

await assertNavigation(container, resolve, resolveLazy);
});
Expand Down
1 change: 1 addition & 0 deletions packages/react-router-dom/__tests__/exports-test.tsx
Expand Up @@ -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", () => {
Expand Down
58 changes: 29 additions & 29 deletions packages/react-router-dom/index.tsx
Expand Up @@ -4,6 +4,7 @@
*/
import * as React from "react";
import type {
FutureConfig,
Location,
NavigateOptions,
NavigationType,
Expand All @@ -26,14 +27,15 @@ 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 {
BrowserHistory,
Fetcher,
FormEncType,
FormMethod,
FutureConfig,
FutureConfig as RouterFutureConfig,
GetScrollRestorationKeyFunction,
HashHistory,
History,
Expand Down Expand Up @@ -209,7 +211,7 @@ declare global {

interface DOMRouterOpts {
basename?: string;
future?: Partial<Omit<FutureConfig, "v7_prependBasename">>;
future?: Partial<Omit<RouterFutureConfig, "v7_prependBasename">>;
hydrationData?: HydrationState;
window?: Window;
}
Expand Down Expand Up @@ -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 `<Router>` for use in web browsers. Provides the cleanest URLs.
*/
export function BrowserRouter({
basename,
children,
future,
window,
}: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
Expand All @@ -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]);
Expand All @@ -362,14 +348,20 @@ export function BrowserRouter({
export interface HashRouterProps {
basename?: string;
children?: React.ReactNode;
future?: FutureConfig;
window?: Window;
}

/**
* A `<Router>` 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<HashHistory>();
if (historyRef.current == null) {
historyRef.current = createHashHistory({ window, v5Compat: true });
Expand All @@ -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]);
Expand All @@ -405,6 +398,7 @@ export function HashRouter({ basename, children, window }: HashRouterProps) {
export interface HistoryRouterProps {
basename?: string;
children?: React.ReactNode;
future?: FutureConfig;
history: History;
}

Expand All @@ -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]);
Expand Down
1 change: 1 addition & 0 deletions packages/react-router-native/__tests__/exports-test.tsx
Expand Up @@ -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", () => {
Expand Down

0 comments on commit 5d9be06

Please sign in to comment.