migrate router to react-router

This commit is contained in:
Norbert de Langen 2021-10-21 15:57:44 +02:00
parent c15ac11f70
commit 5f0dce2545
No known key found for this signature in database
GPG Key ID: E49F18EF96C393F2
13 changed files with 157 additions and 107 deletions

View File

@ -38,7 +38,6 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@reach/router": "^1.3.4",
"@storybook/channels": "6.4.0-beta.15",
"@storybook/client-logger": "6.4.0-beta.15",
"@storybook/core-events": "6.4.0-beta.15",
@ -46,7 +45,6 @@
"@storybook/router": "6.4.0-beta.15",
"@storybook/semver": "^7.3.2",
"@storybook/theming": "6.4.0-beta.15",
"@types/reach__router": "^1.3.7",
"core-js": "^3.8.2",
"fast-deep-equal": "^3.1.3",
"global": "^4.4.0",

View File

@ -18,7 +18,7 @@ import {
SHARED_STATE_SET,
SET_STORIES,
} from '@storybook/core-events';
import { RenderData as RouterData } from '@storybook/router';
import { RouterData } from '@storybook/router';
import { Listener } from '@storybook/channels';
import { createContext } from './context';

View File

@ -1,5 +1,5 @@
import { ReactElement } from 'react';
import { WindowLocation } from '@reach/router';
import type { RenderData } from '@storybook/router';
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
@ -35,13 +35,13 @@ export interface RenderOptions {
export interface RouteOptions {
storyId: string;
viewMode: ViewMode;
location: WindowLocation;
location: RenderData['location'];
path: string;
}
export interface MatchOptions {
storyId: string;
viewMode: ViewMode;
location: WindowLocation;
location: RenderData['location'];
path: string;
}

View File

@ -1,4 +1,3 @@
import { navigate as navigateRouter, NavigateOptions } from '@reach/router';
import { once } from '@storybook/client-logger';
import {
NAVIGATE_URL,
@ -6,7 +5,7 @@ import {
SET_CURRENT_STORY,
GLOBALS_UPDATED,
} from '@storybook/core-events';
import { queryFromLocation, navigate as queryNavigate, buildArgsParam } from '@storybook/router';
import { queryFromLocation, buildArgsParam, NavigateOptions } from '@storybook/router';
import { toId, sanitize } from '@storybook/csf';
import deepEqual from 'fast-deep-equal';
import global from 'global';
@ -28,15 +27,6 @@ const parseBoolean = (value: string) => {
return undefined;
};
const navigateTo = (path: string, queryParams: Record<string, string> = {}, options = {}) => {
const params = Object.entries(queryParams)
.filter(([, v]) => v)
.sort(([a], [b]) => (a < b ? -1 : 1))
.map(([k, v]) => `${k}=${v}`);
const to = [path, ...params].join('&');
return queryNavigate(to, options);
};
// Initialize the state based on the URL.
// NOTE:
// Although we don't change the URL when you change the state, we do support setting initial state
@ -130,7 +120,7 @@ export interface QueryParams {
}
export interface SubAPI {
navigateUrl: (url: string, options: NavigateOptions<{}>) => void;
navigateUrl: (url: string, options: NavigateOptions) => void;
getQueryParam: (key: string) => string | undefined;
getUrlState: () => {
queryParams: QueryParams;
@ -143,6 +133,15 @@ export interface SubAPI {
}
export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...rest }) => {
const navigateTo = (path: string, queryParams: Record<string, string> = {}, options = {}) => {
const params = Object.entries(queryParams)
.filter(([, v]) => v)
.sort(([a], [b]) => (a < b ? -1 : 1))
.map(([k, v]) => `${k}=${v}`);
const to = [path, ...params].join('&');
return navigate(to, options);
};
const api: SubAPI = {
getQueryParam(key) {
const { customQueryParams } = store.getState();
@ -167,8 +166,8 @@ export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...r
const equal = deepEqual(customQueryParams, update);
if (!equal) store.setState({ customQueryParams: update });
},
navigateUrl(url: string, options: NavigateOptions<{}>) {
navigateRouter(url, options);
navigateUrl(url, options) {
navigate(url, { ...options, plain: true });
},
};

View File

@ -1,5 +1,5 @@
# Storybook Router
Storybook Router is a wrapper library for reach/router.
Storybook Router is a wrapper library for react-router.
It ensures a single version of the router is used everywhere.
It also includes some ready to use utils to read the path, query, viewMode and storyId from location.

View File

@ -40,15 +40,16 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@reach/router": "^1.3.4",
"@storybook/client-logger": "6.4.0-beta.15",
"@types/reach__router": "^1.3.7",
"core-js": "^3.8.2",
"fast-deep-equal": "^3.1.3",
"global": "^4.4.0",
"history": "^5.0.1",
"lodash": "^4.17.20",
"memoizerific": "^1.11.3",
"qs": "^6.10.0",
"react-router": "^6.0.0-beta.7",
"react-router-dom": "^6.0.0-beta.7",
"ts-dedent": "^2.0.0"
},
"peerDependencies": {

View File

@ -1,17 +1,7 @@
import global from 'global';
import React, { ReactNode } from 'react';
import React, { ReactNode, useCallback } from 'react';
import {
Link,
Location,
navigate,
LocationProvider,
RouteComponentProps,
LocationContext,
NavigateFn,
NavigateOptions,
History,
} from '@reach/router';
import { Link, BrowserRouter, useNavigate, useLocation, NavigateOptions } from 'react-router-dom';
import { ToggleVisibility } from './visibility';
import { queryFromString, parsePath, getMatch, StoryData } from './utils';
@ -22,9 +12,12 @@ interface Other extends StoryData {
singleStory?: boolean;
}
export type RenderData = Pick<LocationContext, 'location'> &
Partial<Pick<LocationContext, 'navigate'>> &
Other;
export type RouterData = {
location: Partial<Location>;
navigate: ReturnType<typeof useQueryNavigate>;
} & Other;
export type RenderData = Pick<RouterData, 'location'> & Other;
interface MatchingData {
match: null | { path: string };
@ -52,8 +45,26 @@ export interface QueryLinkProps {
const getBase = () => `${document.location.pathname}?`;
const queryNavigate: NavigateFn = (to: string | number, options?: NavigateOptions<{}>) =>
typeof to === 'number' ? navigate(to) : navigate(`${getBase()}path=${to}`, options);
type ExpandedNavigateOptions = NavigateOptions & { plain?: boolean };
// const queryNavigate: NavigateFn = (to: string | number, options?: NavigateOptions<{}>) =>
// typeof to === 'number' ? navigate(to) : navigate(`${getBase()}path=${to}`, options);
const useQueryNavigate = () => {
const navigate = useNavigate();
return useCallback((to: string | number, options?: ExpandedNavigateOptions) => {
if (typeof to === 'string') {
const target = options?.plain ? to : `?path=${to}`;
return navigate(target, options);
}
if (typeof to === 'number') {
return navigate(to);
}
return undefined;
}, []);
};
// A component that will navigate to a new location/path when clicked
const QueryLink = ({ to, children, ...rest }: QueryLinkProps) => (
@ -65,24 +76,24 @@ QueryLink.displayName = 'QueryLink';
// A render-prop component where children is called with a location
// and will be called whenever it changes when it changes
const QueryLocation = ({ children }: QueryLocationProps) => (
<Location>
{({ location }: RouteComponentProps): ReactNode => {
const { path, singleStory } = queryFromString(location.search);
const { viewMode, storyId, refId } = parsePath(path);
const QueryLocation = ({ children }: QueryLocationProps) => {
const location = useLocation();
const { path, singleStory } = queryFromString(location.search);
const { viewMode, storyId, refId } = parsePath(path);
return children({
return (
<>
{children({
path,
location,
navigate: queryNavigate,
viewMode,
storyId,
refId,
singleStory: singleStory === 'true',
});
}}
</Location>
);
})}
</>
);
};
QueryLocation.displayName = 'QueryLocation';
// A render-prop component for rendering when a certain path is hit.
@ -117,6 +128,9 @@ export { QueryLink as Link };
export { QueryMatch as Match };
export { QueryLocation as Location };
export { Route };
export { queryNavigate as navigate };
export { LocationProvider };
export type { History };
export { useQueryNavigate as useNavigate };
export { BrowserRouter as LocationProvider };
export { useNavigate as usePlainNavigate };
// eslint-disable-next-line no-undef
export type { ExpandedNavigateOptions as NavigateOptions };

View File

@ -139,7 +139,7 @@ interface Query {
export const queryFromString = memoize(1000)(
(s: string): Query => qs.parse(s, { ignoreQueryPrefix: true })
);
export const queryFromLocation = (location: { search: string }) => queryFromString(location.search);
export const queryFromLocation = (location: Partial<Location>) => queryFromString(location.search);
export const stringifyQuery = (query: Query) =>
qs.stringify(query, { addQueryPrefix: true, encode: false });

View File

@ -1,5 +1,4 @@
import React from 'react';
import { createMemorySource, createHistory } from '@reach/router';
import { Root as App } from './index';
import { PrettyFakeProvider, FakeProvider } from './FakeProvider';
@ -13,12 +12,8 @@ export default {
},
};
const history = createHistory(createMemorySource('/?path=/story/story--id'));
export const Default = () => (
<App provider={(new FakeProvider() as unknown) as Provider} history={history} />
);
export const Default = () => <App provider={(new FakeProvider() as unknown) as Provider} />;
export const LoadingState = () => (
<App provider={(new PrettyFakeProvider() as unknown) as Provider} history={history} />
<App provider={(new PrettyFakeProvider() as unknown) as Provider} />
);

View File

@ -1,8 +1,8 @@
import React from 'react';
import { Provider as ManagerProvider, Combo, Consumer } from '@storybook/api';
import { createMemorySource, createHistory } from '@reach/router';
import { Location, LocationProvider } from '@storybook/router';
import { ThemeProvider, ensure as ensureTheme, themes } from '@storybook/theming';
import { DecoratorFn } from '@storybook/react';
@ -18,13 +18,17 @@ export default {
component: Preview,
decorators: [
((StoryFn, c) => (
<LocationProvider
key="location.provider"
history={createHistory(createMemorySource('/?path=/story/story--id'))}
>
<LocationProvider key="location.provider">
<Location key="location.consumer">
{(locationData) => (
<ManagerProvider key="manager" provider={provider} {...locationData} docsMode={false}>
<ManagerProvider
key="manager"
provider={provider}
{...locationData}
docsMode={false}
path="/story/story--id"
navigate={() => {}}
>
<ThemeProvider key="theme.provider" theme={ensureTheme(themes.light)}>
<StoryFn {...c} />
</ThemeProvider>

View File

@ -7,7 +7,7 @@ import { Consumer, Combo, API, Story, Group, State, merge } from '@storybook/api
import { shortcutToHumanString } from '@storybook/api/shortcut';
import { addons, Addon, types } from '@storybook/addons';
import { Location, RenderData } from '@storybook/router';
import { Location, RenderData, useNavigate } from '@storybook/router';
import { zoomTool } from './tools/zoom';
import * as S from './utils/components';

View File

@ -1,8 +1,8 @@
import global from 'global';
import React, { FunctionComponent } from 'react';
import React, { FC, FunctionComponent } from 'react';
import ReactDOM from 'react-dom';
import { Location, LocationProvider, History } from '@storybook/router';
import { Location, LocationProvider, useNavigate } from '@storybook/router';
import { Provider as ManagerProvider, Combo } from '@storybook/api';
import { ThemeProvider, ensure as ensureTheme } from '@storybook/theming';
import { HelmetProvider } from 'react-helmet-async';
@ -33,45 +33,53 @@ export interface RootProps {
history?: History;
}
export const Root: FunctionComponent<RootProps> = ({ provider, history }) => (
export const Root: FunctionComponent<RootProps> = ({ provider }) => (
<Container key="container">
<HelmetProvider key="helmet.Provider">
<LocationProvider key="location.provider" history={history}>
<Location key="location.consumer">
{(locationData) => (
<ManagerProvider
key="manager"
provider={provider}
{...locationData}
docsMode={getDocsMode()}
>
{({ state, api }: Combo) => {
const panelCount = Object.keys(api.getPanels()).length;
const story = api.getData(state.storyId, state.refId);
const isLoading = story
? !!state.refs[state.refId] && !state.refs[state.refId].ready
: !state.storiesFailed && !state.storiesConfigured;
return (
<ThemeProvider key="theme.provider" theme={ensureTheme(state.theme)}>
<App
key="app"
viewMode={state.viewMode}
layout={isLoading ? { ...state.layout, showPanel: false } : state.layout}
panelCount={panelCount}
docsOnly={story && story.parameters && story.parameters.docsOnly}
/>
</ThemeProvider>
);
}}
</ManagerProvider>
)}
</Location>
<LocationProvider key="location.provider">
<Main provider={provider} />
</LocationProvider>
</HelmetProvider>
</Container>
);
const Main: FC<{ provider: Provider }> = ({ provider }) => {
const navigate = useNavigate();
return (
<Location key="location.consumer">
{(locationData) => (
<ManagerProvider
key="manager"
provider={provider}
{...locationData}
navigate={navigate}
docsMode={getDocsMode()}
>
{({ state, api }: Combo) => {
const panelCount = Object.keys(api.getPanels()).length;
const story = api.getData(state.storyId, state.refId);
const isLoading = story
? !!state.refs[state.refId] && !state.refs[state.refId].ready
: !state.storiesFailed && !state.storiesConfigured;
return (
<ThemeProvider key="theme.provider" theme={ensureTheme(state.theme)}>
<App
key="app"
viewMode={state.viewMode}
layout={isLoading ? { ...state.layout, showPanel: false } : state.layout}
panelCount={panelCount}
docsOnly={story && story.parameters && story.parameters.docsOnly}
/>
</ThemeProvider>
);
}}
</ManagerProvider>
)}
</Location>
);
};
function renderStorybookUI(domNode: HTMLElement, provider: Provider) {
if (!(provider instanceof Provider)) {
throw new Error('provider is not extended from the base Provider');

View File

@ -7586,7 +7586,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@storybook/api@workspace:lib/api"
dependencies:
"@reach/router": ^1.3.4
"@storybook/channels": 6.4.0-beta.15
"@storybook/client-logger": 6.4.0-beta.15
"@storybook/core-events": 6.4.0-beta.15
@ -7595,7 +7594,6 @@ __metadata:
"@storybook/semver": ^7.3.2
"@storybook/theming": 6.4.0-beta.15
"@types/lodash": ^4.14.167
"@types/reach__router": ^1.3.7
"@types/semver": ^7.3.4
core-js: ^3.8.2
fast-deep-equal: ^3.1.3
@ -9021,15 +9019,16 @@ __metadata:
version: 0.0.0-use.local
resolution: "@storybook/router@workspace:lib/router"
dependencies:
"@reach/router": ^1.3.4
"@storybook/client-logger": 6.4.0-beta.15
"@types/reach__router": ^1.3.7
core-js: ^3.8.2
fast-deep-equal: ^3.1.3
global: ^4.4.0
history: ^5.0.1
lodash: ^4.17.20
memoizerific: ^1.11.3
qs: ^6.10.0
react-router: ^6.0.0-beta.7
react-router-dom: ^6.0.0-beta.7
ts-dedent: ^2.0.0
peerDependencies:
react: ^16.8.0 || ^17.0.0
@ -25346,6 +25345,15 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"history@npm:^5.0.1":
version: 5.0.1
resolution: "history@npm:5.0.1"
dependencies:
"@babel/runtime": ^7.7.6
checksum: a36e20f3513acf5c3bd2c0edd5790cbf8ef6befb754595a7c99ee70b07afd6eef07396ec2f90f50e0602b580e4fbe6fe5950e812e4bb08fa541e296f9434a566
languageName: node
linkType: hard
"hmac-drbg@npm:^1.0.1":
version: 1.0.1
resolution: "hmac-drbg@npm:1.0.1"
@ -37953,6 +37961,29 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"react-router-dom@npm:^6.0.0-beta.7":
version: 6.0.0-beta.7
resolution: "react-router-dom@npm:6.0.0-beta.7"
dependencies:
react-router: 6.0.0-beta.7
peerDependencies:
history: ">=5"
react: ">=16.8"
react-dom: ">=16.8"
checksum: baf821316e5b1bb6f82f986e2a603123e5300264a036ea9ddeef4687c77daa4dd8a1fade4f890b6b1e5241745bc98791107cf8193596473e42be873ae6674dcc
languageName: node
linkType: hard
"react-router@npm:6.0.0-beta.7, react-router@npm:^6.0.0-beta.7":
version: 6.0.0-beta.7
resolution: "react-router@npm:6.0.0-beta.7"
peerDependencies:
history: ">=5"
react: ">=16.8"
checksum: f28071978b9329d3c3c998dd86a2e2ef59bfded6fa62976705959ffbfe6e0cb2b739f8dc364cd0deac69968444a3218c3cd94f48a4ed7753db91967321eeb9f1
languageName: node
linkType: hard
"react-scripts@npm:3.4.4":
version: 3.4.4
resolution: "react-scripts@npm:3.4.4"