Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add future.v7_startTransition flag #10596

Merged
merged 7 commits into from Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/v7-start-transition.md
@@ -0,0 +1,26 @@
---
"react-router": minor
"react-router-dom": minor
Comment on lines +2 to +3
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a new future flag it'll be a new 6.13.0 minor release

---

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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this feel confusing? Some flags are router-only and have nothing to do with rendering, others are render-only so I kept them split, but this does mean we export a FutureConfig from both @remix-run/router and react-router.


### `@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