Skip to content

Commit 1662370

Browse files
TkDodoandrewshie-sentry
authored andcommitted
ref(ui): decouple scraps/link from sentry specific code (#102644)
This PR introduces a `LinkBehaviorContext` that allows us to decouple sentry specific code from our design-system `scraps`. In this first iteration, the only sentry specific things were: - `locationDescriptorToTo` - a call to `useLocation` to fall-back to anchor rendering if there is no location. Honestly, I don’t know why / if we need that because when would have no `useLocation` ? I do plan to add more sentry specific link behavior, like [intent prefetching](#102574), but this de-coupling is pre-requisite for that.
1 parent 5e14b79 commit 1662370

File tree

9 files changed

+95
-33
lines changed

9 files changed

+95
-33
lines changed

static/app/components/core/link/link.tsx

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
import {
2-
Link as RouterLink,
3-
type LinkProps as ReactRouterLinkProps,
4-
} from 'react-router-dom';
1+
import {type LinkProps as ReactRouterLinkProps} from 'react-router-dom';
52
import isPropValid from '@emotion/is-prop-valid';
63
import {css, type Theme} from '@emotion/react';
74
import styled from '@emotion/styled';
85
import type {LocationDescriptor} from 'history';
96

10-
import {locationDescriptorToTo} from 'sentry/utils/reactRouter6Compat/location';
11-
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
12-
import {useLocation} from 'sentry/utils/useLocation';
7+
import {useLinkBehavior} from './linkBehaviorContext';
138

149
export interface LinkProps
1510
extends React.RefAttributes<HTMLAnchorElement>,
@@ -61,26 +56,19 @@ const getLinkStyles = ({
6156
`;
6257

6358
const Anchor = styled('a', {
64-
shouldForwardProp: prop =>
65-
typeof prop === 'string' && isPropValid(prop) && prop !== 'disabled',
59+
shouldForwardProp: prop => isPropValid(prop) && prop !== 'disabled',
6660
})<{disabled?: LinkProps['disabled']}>`
6761
${getLinkStyles}
6862
`;
6963

70-
/**
71-
* A context-aware version of Link (from react-router) that falls
72-
* back to <a> if there is no router present
73-
*/
74-
export const Link = styled(({disabled, to, ...props}: LinkProps) => {
75-
const location = useLocation();
64+
export const Link = styled((props: LinkProps) => {
65+
const {Component, behavior} = useLinkBehavior(props);
7666

77-
if (disabled || !location) {
67+
if (props.disabled || Component === 'a') {
7868
return <Anchor {...props} />;
7969
}
8070

81-
return (
82-
<RouterLink to={locationDescriptorToTo(normalizeUrl(to, location))} {...props} />
83-
);
71+
return <Component {...behavior()} />;
8472
})`
8573
${getLinkStyles}
8674
`;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {createContext, useContext, type FunctionComponent} from 'react';
2+
3+
import type {LinkProps} from './link';
4+
5+
const LinkBehaviorContext = createContext<{
6+
behavior: (props: LinkProps) => LinkProps;
7+
component: FunctionComponent<LinkProps> | 'a';
8+
}>({
9+
component: 'a',
10+
behavior: props => props,
11+
});
12+
13+
export const LinkBehaviorContextProvider = LinkBehaviorContext.Provider;
14+
15+
export const useLinkBehavior = (props: LinkProps) => {
16+
const {component, behavior} = useContext(LinkBehaviorContext);
17+
18+
return {Component: component, behavior: () => behavior(props)};
19+
};

static/app/main.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {FrontendVersionProvider} from 'sentry/components/frontendVersionContext'
1010
import {ThemeAndStyleProvider} from 'sentry/components/themeAndStyleProvider';
1111
import {SENTRY_RELEASE_VERSION, USE_REACT_QUERY_DEVTOOL} from 'sentry/constants';
1212
import {routes} from 'sentry/routes';
13-
import {SentryTrackingProvider} from 'sentry/tracking';
1413
import {DANGEROUS_SET_REACT_ROUTER_6_HISTORY} from 'sentry/utils/browserHistory';
1514

1615
function buildRouter() {
@@ -28,13 +27,11 @@ function Main() {
2827
<AppQueryClientProvider>
2928
<FrontendVersionProvider releaseVersion={SENTRY_RELEASE_VERSION ?? null}>
3029
<ThemeAndStyleProvider>
31-
<SentryTrackingProvider>
32-
<NuqsAdapter defaultOptions={{shallow: false}}>
33-
<CommandPaletteProvider>
34-
<RouterProvider router={router} />
35-
</CommandPaletteProvider>
36-
</NuqsAdapter>
37-
</SentryTrackingProvider>
30+
<NuqsAdapter defaultOptions={{shallow: false}}>
31+
<CommandPaletteProvider>
32+
<RouterProvider router={router} />
33+
</CommandPaletteProvider>
34+
</NuqsAdapter>
3835
{USE_REACT_QUERY_DEVTOOL && (
3936
<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />
4037
)}

static/app/routes.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import memoize from 'lodash/memoize';
33

44
import {EXPERIMENTAL_SPA} from 'sentry/constants';
55
import {t} from 'sentry/locale';
6+
import {ScrapsProviders} from 'sentry/scrapsProviders';
67
import HookStore from 'sentry/stores/hookStore';
78
import type {HookName} from 'sentry/types/hooks';
89
import errorHandler from 'sentry/utils/errorHandler';
@@ -3066,7 +3067,13 @@ function buildRoutes(): RouteObject[] {
30663067
};
30673068

30683069
const appRoutes: SentryRouteObject = {
3069-
component: ProvideAriaRouter,
3070+
component: ({children}: {children: React.ReactNode}) => {
3071+
return (
3072+
<ProvideAriaRouter>
3073+
<ScrapsProviders>{children}</ScrapsProviders>
3074+
</ProvideAriaRouter>
3075+
);
3076+
},
30703077
deprecatedRouteProps: true,
30713078
children: [
30723079
experimentalSpaRoutes,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {SentryLinkBehaviorProvider} from './link';
2+
import {SentryTrackingProvider} from './tracking';
3+
4+
export function ScrapsProviders({children}: {children: React.ReactNode}) {
5+
return (
6+
<SentryTrackingProvider>
7+
<SentryLinkBehaviorProvider>{children}</SentryLinkBehaviorProvider>
8+
</SentryTrackingProvider>
9+
);
10+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {useMemo} from 'react';
2+
import {Link as RouterLink} from 'react-router-dom';
3+
4+
import type {LinkProps} from '@sentry/scraps/link';
5+
import {LinkBehaviorContextProvider} from '@sentry/scraps/link/linkBehaviorContext';
6+
7+
import {locationDescriptorToTo} from 'sentry/utils/reactRouter6Compat/location';
8+
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
9+
import {useLocation} from 'sentry/utils/useLocation';
10+
11+
export function SentryLinkBehaviorProvider({children}: {children: React.ReactNode}) {
12+
const location = useLocation();
13+
14+
return (
15+
<LinkBehaviorContextProvider
16+
value={useMemo(
17+
() => ({
18+
component: RouterLink,
19+
behavior: ({to, ...props}: LinkProps) => {
20+
const normalizedTo = locationDescriptorToTo(normalizeUrl(to, location));
21+
22+
return {
23+
to: normalizedTo,
24+
...props,
25+
};
26+
},
27+
}),
28+
[location]
29+
)}
30+
>
31+
{children}
32+
</LinkBehaviorContextProvider>
33+
);
34+
}
File renamed without changes.

tests/js/sentry-test/reactTestingLibrary.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import * as qs from 'query-string';
2323
import {LocationFixture} from 'sentry-fixture/locationFixture';
2424
import {ThemeFixture} from 'sentry-fixture/theme';
2525

26-
import {makeTestQueryClient} from 'sentry-test/queryClient';
27-
2826
import {CommandPaletteProvider} from 'sentry/components/commandPalette/context';
2927
import {GlobalDrawer} from 'sentry/components/globalDrawer';
3028
import GlobalModal from 'sentry/components/globalModal';
@@ -43,6 +41,8 @@ import {instrumentUserEvent} from '../instrumentedEnv/userEventIntegration';
4341

4442
import {initializeOrg} from './initializeOrg';
4543
import {SentryNuqsTestingAdapter} from './nuqsTestingAdapter';
44+
import {makeTestQueryClient} from './queryClient';
45+
import {ScrapsTestingProviders} from './scrapsTestingProviders';
4646

4747
interface ProviderOptions {
4848
/**
@@ -218,9 +218,11 @@ function makeAllTheProviders(options: ProviderOptions) {
218218
<CacheProvider value={{...cache, compat: true}}>
219219
<QueryClientProvider client={makeTestQueryClient()}>
220220
<SentryNuqsTestingAdapter defaultOptions={{shallow: false}}>
221-
<CommandPaletteProvider>
222-
<ThemeProvider theme={ThemeFixture()}>{wrappedContent}</ThemeProvider>
223-
</CommandPaletteProvider>
221+
<ScrapsTestingProviders>
222+
<CommandPaletteProvider>
223+
<ThemeProvider theme={ThemeFixture()}>{wrappedContent}</ThemeProvider>
224+
</CommandPaletteProvider>
225+
</ScrapsTestingProviders>
224226
</SentryNuqsTestingAdapter>
225227
</QueryClientProvider>
226228
</CacheProvider>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {SentryLinkBehaviorProvider} from 'sentry/scrapsProviders/link';
2+
3+
export function ScrapsTestingProviders({children}: {children: React.ReactNode}) {
4+
return <SentryLinkBehaviorProvider>{children}</SentryLinkBehaviorProvider>;
5+
}

0 commit comments

Comments
 (0)