Skip to content

Commit

Permalink
feat(rr6): Upgrade React Router 3 → 6 (#70632)
Browse files Browse the repository at this point in the history
This is tracked by getsentry/frontend-tsc#23
  • Loading branch information
evanpurkhiser committed Aug 27, 2024
1 parent e369515 commit 57035b3
Show file tree
Hide file tree
Showing 19 changed files with 752 additions and 57 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"invariant": "^2.2.4",
"ios-device-list": "1.1.37",
"jed": "^1.1.0",
"jest-fetch-mock": "^3.0.3",
"js-beautify": "^1.15.1",
"js-cookie": "3.0.1",
"less": "^4.1.3",
Expand Down Expand Up @@ -156,6 +157,7 @@
"react-mentions": "4.4.10",
"react-popper": "^2.3.0",
"react-router": "3.2.6",
"react-router-dom": "^6.23.0",
"react-select": "4.3.1",
"react-sparklines": "1.7.0",
"react-virtualized": "^9.22.5",
Expand Down
35 changes: 26 additions & 9 deletions static/app/bootstrap/initializeSdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import type {Config} from 'sentry/types/system';
import {addExtraMeasurements, addUIElementTag} from 'sentry/utils/performanceForSentry';
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
import {getErrorDebugIds} from 'sentry/utils/getErrorDebugIds';
import {
createRoutesFromChildren,
matchRoutes,
useLocation,
useNavigationType,
} from 'react-router-dom';
import {useEffect} from 'react';

const SPA_MODE_ALLOW_URLS = [
'localhost',
Expand Down Expand Up @@ -49,20 +56,30 @@ const shouldOverrideBrowserProfiling = window?.__initialData?.user?.isSuperuser;
* (e.g. `static/views/integrationPipeline`)
*/
function getSentryIntegrations(routes?: Function) {
const reactRouterIntegration = window.__SENTRY_USING_REACT_ROUTER_SIX
? Sentry.reactRouterV6BrowserTracingIntegration({
useEffect: useEffect,
useLocation: useLocation,
useNavigationType: useNavigationType,
createRoutesFromChildren: createRoutesFromChildren,
matchRoutes: matchRoutes,
})
: Sentry.reactRouterV3BrowserTracingIntegration({
history: browserHistory as any,
routes: typeof routes === 'function' ? createRoutes(routes()) : [],
match,
enableLongAnimationFrame: true,
_experiments: {
enableInteractions: false,
},
});

const integrations = [
Sentry.extraErrorDataIntegration({
// 6 is arbitrary, seems like a nice number
depth: 6,
}),
Sentry.reactRouterV3BrowserTracingIntegration({
history: browserHistory as any,
routes: typeof routes === 'function' ? createRoutes(routes()) : [],
match,
enableLongAnimationFrame: true,
_experiments: {
enableInteractions: false,
},
}),
reactRouterIntegration,
Sentry.browserProfilingIntegration(),
Sentry.thirdPartyErrorFilterIntegration({
filterKeys: ['sentry-spa'],
Expand Down
12 changes: 12 additions & 0 deletions static/app/components/links/link.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {forwardRef} from 'react';
import {Link as RouterLink} from 'react-router';
import {Link as Router6Link} from 'react-router-dom';
import styled from '@emotion/styled';
import type {LocationDescriptor} from 'history';

import {locationDescriptorToTo} from 'sentry/utils/reactRouter6Compat/location';
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
import {useLocation} from 'sentry/utils/useLocation';

Expand Down Expand Up @@ -46,6 +48,16 @@ function BaseLink({disabled, to, forwardedRef, ...props}: LinkProps): React.Reac
to = normalizeUrl(to, location);

if (!disabled && location) {
if (window.__SENTRY_USING_REACT_ROUTER_SIX) {
return (
<Router6Link
to={locationDescriptorToTo(to)}
ref={forwardedRef as any}
{...props}
/>
);
}

return <RouterLink to={to} ref={forwardedRef as any} {...props} />;
}

Expand Down
38 changes: 29 additions & 9 deletions static/app/components/links/listLink.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import {Link as RouterLink} from 'react-router';
import {NavLink} from 'react-router-dom';
import styled from '@emotion/styled';
import classNames from 'classnames';
import type {LocationDescriptor} from 'history';

import {locationDescriptorToTo} from 'sentry/utils/reactRouter6Compat/location';
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
import {useLocation} from 'sentry/utils/useLocation';
import useRouter from 'sentry/utils/useRouter';

type LinkProps = Omit<React.ComponentProps<typeof RouterLink>, 'to'>;

type Props = LinkProps & {
interface ListLinkProps
extends Omit<
React.DetailedHTMLProps<React.HTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>,
'href' | 'target' | 'as' | 'css' | 'ref'
> {
/**
* Link target. We don't want to expose the ToLocationFunction on this component.
*/
Expand All @@ -19,7 +24,7 @@ type Props = LinkProps & {
* Should be should be supplied by the parent component
*/
isActive?: (location: LocationDescriptor, indexOnly?: boolean) => boolean;
};
}

function ListLink({
children,
Expand All @@ -29,18 +34,33 @@ function ListLink({
index = false,
disabled = false,
...props
}: Props) {
}: ListLinkProps) {
const router = useRouter();
const location = useLocation();
const targetLocation = typeof to === 'string' ? {pathname: to} : to;
const target = normalizeUrl(targetLocation);

const active = isActive?.(target, index) ?? router.isActive(target, index);
const active =
isActive?.(target, index) ??
// XXX(epurkhiser): our shim for router.isActive will throw an error in
// react-router 6. Fallback to manually checking if the path is active
(window.__SENTRY_USING_REACT_ROUTER_SIX
? location.pathname === (typeof target === 'string' ? target : target.pathname)
: router.isActive(target, index));

const link = window.__SENTRY_USING_REACT_ROUTER_SIX ? (
<NavLink {...props} to={disabled ? '' : locationDescriptorToTo(target)}>
{children}
</NavLink>
) : (
<RouterLink {...props} onlyActiveOnIndex={index} to={disabled ? '' : target}>
{children}
</RouterLink>
);

return (
<StyledLi className={classNames({active}, className)} disabled={disabled}>
<RouterLink {...props} onlyActiveOnIndex={index} to={disabled ? '' : target}>
{children}
</RouterLink>
{link}
</StyledLi>
);
}
Expand Down
26 changes: 21 additions & 5 deletions static/app/main.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import {Router, RouterContext} from 'react-router';
import {createBrowserRouter, RouterProvider} from 'react-router-dom';
import {wrapCreateBrowserRouter} from '@sentry/react';
import {ReactQueryDevtools} from '@tanstack/react-query-devtools';

import DemoHeader from 'sentry/components/demo/demoHeader';
import {OnboardingContextProvider} from 'sentry/components/onboarding/onboardingContext';
import {ThemeAndStyleProvider} from 'sentry/components/themeAndStyleProvider';
import {USE_REACT_QUERY_DEVTOOL} from 'sentry/constants';
import {routes} from 'sentry/routes';
import {routes, routes6} from 'sentry/routes';
import ConfigStore from 'sentry/stores/configStore';
import {browserHistory} from 'sentry/utils/browserHistory';
import {
browserHistory,
DANGEROUS_SET_REACT_ROUTER_6_HISTORY,
} from 'sentry/utils/browserHistory';
import {
DEFAULT_QUERY_CLIENT_CONFIG,
QueryClient,
Expand All @@ -29,15 +34,26 @@ function renderRouter(props: any) {

const queryClient = new QueryClient(DEFAULT_QUERY_CLIENT_CONFIG);

const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createBrowserRouter);
const router = sentryCreateBrowserRouter(routes6);

if (window.__SENTRY_USING_REACT_ROUTER_SIX) {
DANGEROUS_SET_REACT_ROUTER_6_HISTORY(router);
}

function Main() {
return (
<ThemeAndStyleProvider>
<QueryClientProvider client={queryClient}>
<OnboardingContextProvider>
{ConfigStore.get('demoMode') && <DemoHeader />}
<Router history={browserHistory} render={renderRouter}>
{routes()}
</Router>
{window.__SENTRY_USING_REACT_ROUTER_SIX ? (
<RouterProvider router={router} />
) : (
<Router history={browserHistory} render={renderRouter}>
{routes()}
</Router>
)}
</OnboardingContextProvider>
{USE_REACT_QUERY_DEVTOOL && (
<ReactQueryDevtools initialIsOpen={false} position="bottom-left" />
Expand Down
21 changes: 12 additions & 9 deletions static/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import RouteNotFound from 'sentry/views/routeNotFound';
import SettingsWrapper from 'sentry/views/settings/components/settingsWrapper';

import {IndexRoute, Route} from './components/route';
import {buildReactRouter6Routes} from './utils/reactRouter6Compat/router';

const hook = (name: HookName) => HookStore.get(name).map(cb => cb());

Expand Down Expand Up @@ -690,15 +691,13 @@ function buildRoutes() {
)}
/>
)}
{USING_CUSTOMER_DOMAIN && (
<Route
path="/settings/organization/"
name={t('General')}
component={make(
() => import('sentry/views/settings/organizationGeneralSettings')
)}
/>
)}
<Route
path="organization/"
name={t('General')}
component={make(
() => import('sentry/views/settings/organizationGeneralSettings')
)}
/>
<Route
path="projects/"
name={t('Projects')}
Expand Down Expand Up @@ -2283,6 +2282,10 @@ function buildRoutes() {
// when the app renders Main. Memoize to avoid rebuilding the route tree.
export const routes = memoize(buildRoutes);

// XXX(epurkhiser): Transforms the legacy react-router 3 routest tree into a
// react-router 6 style routes tree.
export const routes6 = buildReactRouter6Routes(buildRoutes());

// Exported for use in tests.
export {buildRoutes};

Expand Down
5 changes: 5 additions & 0 deletions static/app/types/system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ declare global {
* Used by webpack-devserver + html-webpack
*/
__SENTRY_DEV_UI?: boolean;
/**
* Use react-router v6 in compatability mode. This exists while we migrate
* off of react-router v3.
*/
__SENTRY_USING_REACT_ROUTER_SIX?: boolean;
/**
* Sentrys version string
*/
Expand Down
73 changes: 72 additions & 1 deletion static/app/utils/browserHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {browserHistory as react3BrowserHistory} from 'react-router';
import type {Router} from '@remix-run/router/dist/router';
import type {History} from 'history';

import {locationDescriptorToTo} from './reactRouter6Compat/location';

/**
* @deprecated Prefer using useNavigate
Expand All @@ -14,4 +18,71 @@ import {browserHistory as react3BrowserHistory} from 'react-router';
* browserHistory.push({...location, query: {someKey: 1}})
* navigate({...location, query: {someKey: 1}})
*/
export const browserHistory = react3BrowserHistory;
export let browserHistory = react3BrowserHistory;

/**
* This shim sets the global `browserHistory` to a shim object that matches
* react-router 3's browserHistory implementation
*/
export function DANGEROUS_SET_REACT_ROUTER_6_HISTORY(router: Router) {
// XXX(epurkhiser): The router object for react-router 6 has a slightly
// different interface from -router 3 history. We need to shim some of the
// functions to keep things working
const compat6BrowserHistory: History = {
push: to => router.navigate(locationDescriptorToTo(to)),
replace: to => router.navigate(locationDescriptorToTo(to), {replace: true}),
go: n => router.navigate(n),
goBack: () => router.navigate(-1),
goForward: () => router.navigate(1),

// XXX(epurkhiser): react-router 6's BrowserHistory does not let you create
// multiple listeners. This implementation is similar but allows multiple
// listeners.
listen: _listener => {
// eslint-disable-next-line no-console
console.error('browserHistory.listen not implemented on react-router 6 shim');
return () => {};
},

listenBefore: _hook => {
// eslint-disable-next-line no-console
console.error('browserHistory.listenBefore not implemented on react-router 6 shim');
return () => {};
},
transitionTo: _location => {
// eslint-disable-next-line no-console
console.error('browserHistory.transitionTo not implemented on react-router 6 shim');
},
createKey: () => {
// eslint-disable-next-line no-console
console.error('browserHistory.createKey not implemented on react-router 6 shim');
return '';
},
createPath: () => {
// eslint-disable-next-line no-console
console.error('browserHistory.createPath not implemented on react-router 6 shim');
return '';
},
createHref: () => {
// eslint-disable-next-line no-console
console.error('browserHistory.createHref not implemented on react-router 6 shim');
return '';
},
createLocation: () => {
// eslint-disable-next-line no-console
console.error(
'browserHistory.createLocation not implemented on react-router 6 shim'
);
return undefined as any;
},
getCurrentLocation: () => {
// eslint-disable-next-line no-console
console.error(
'browserHistory.getCurrentLocation not implemented on react-router 6 shim'
);
return undefined as any;
},
};

browserHistory = compat6BrowserHistory;
}
Loading

0 comments on commit 57035b3

Please sign in to comment.