Skip to content

Commit c0f735c

Browse files
authored
Allow Redirect component to accept Route object as to (#108)
* Allow Redirect component to accept Route object as 'to' * Adjust flow types and remove 'any' * Fix minor test issue
1 parent a4e0f03 commit c0f735c

File tree

6 files changed

+113
-36
lines changed

6 files changed

+113
-36
lines changed

src/__tests__/unit/controllers/redirect/test.tsx

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { defaultRegistry } from 'react-sweet-state';
66
import { Redirect } from '../../../../controllers/redirect';
77
import { RedirectProps } from '../../../../controllers/redirect/types';
88
import { Router } from '../../../../controllers/router';
9+
import type { Route } from '../../../../common/types';
910

1011
const MockLocation = {
1112
pathname: 'pathname',
@@ -32,6 +33,12 @@ const defaultArgs = {
3233
};
3334

3435
const mockConsole = { ...global.console, warn: jest.fn() };
36+
const mockRoute: Route = {
37+
name: 'test',
38+
component: () => null,
39+
path: '/:id',
40+
query: ['foo'],
41+
};
3542

3643
describe('Redirect', () => {
3744
const mountInRouter = (args: Partial<RedirectProps>) =>
@@ -81,13 +88,54 @@ describe('Redirect', () => {
8188
expect(MockHistory.push).toHaveBeenCalledWith(to);
8289
});
8390

84-
it('should navigate to given route correctly', () => {
85-
const to = '/cool-page';
86-
87-
mountInRouter({ to, push: false });
88-
expect(MockHistory.replace).toHaveBeenCalledWith(to);
89-
expect(MockHistory.push).not.toHaveBeenCalled();
90-
});
91+
it.each([
92+
[
93+
'with `params` and `query`',
94+
{ name: 'a', path: '/:id', query: ['foo'] } as Route,
95+
{ id: '1' },
96+
{ foo: 'bar' },
97+
'/1?foo=bar',
98+
],
99+
[
100+
'with `params`',
101+
{ name: 'b', path: '/:id', query: ['foo'] } as Route,
102+
{ id: '1' },
103+
undefined,
104+
'/1',
105+
],
106+
[
107+
'with `query`',
108+
{ name: 'c', path: '/home', query: ['page'] } as Route,
109+
undefined,
110+
{ page: '1' },
111+
'/home?page=1',
112+
],
113+
[
114+
'without `params` and `query`',
115+
{ name: 'd', path: '/menu', query: ['page'] } as Route,
116+
undefined,
117+
undefined,
118+
'/menu',
119+
],
120+
])(
121+
"doesn't break / throw when rendered with `to` as a Route object, %s",
122+
(_, to, params, query, expected) => {
123+
expect(() => mountInRouter({ to, params, query })).not.toThrow();
124+
expect(MockHistory.push).toHaveBeenCalledWith(expected);
125+
}
126+
);
127+
128+
it.each([
129+
['string', '/cool-page', undefined, undefined, '/cool-page'],
130+
['object', mockRoute, { id: '2' }, { foo: 'bar' }, '/2?foo=bar'],
131+
])(
132+
'should navigate to given route %s correctly',
133+
(_, to, params, query, expected) => {
134+
mountInRouter({ to, query, params, push: false });
135+
expect(MockHistory.replace).toHaveBeenCalledWith(expected);
136+
expect(MockHistory.push).not.toHaveBeenCalled();
137+
}
138+
);
91139

92140
it('should navigate to absolute URLs', () => {
93141
jest.spyOn(window.location, 'assign').mockImplementation(() => jest.fn());
@@ -99,19 +147,29 @@ describe('Redirect', () => {
99147
expect(window.location.assign).toHaveBeenCalledWith(to);
100148
});
101149

102-
it('should not redirect if the location is equivalent to current', () => {
103-
mountInRouter({ to: 'pathname' });
104-
105-
expect(mockConsole.warn).toHaveBeenCalledWith(expect.any(String));
106-
expect(MockHistory.replace).not.toHaveBeenCalled();
107-
expect(MockHistory.push).not.toHaveBeenCalled();
108-
});
109-
110-
it('should use push history correctly', () => {
111-
const to = '/cool-page';
112-
113-
mountInRouter({ to, push: true });
114-
expect(MockHistory.push).toHaveBeenCalledWith(to);
115-
expect(MockHistory.replace).not.toHaveBeenCalled();
116-
});
150+
it.each([
151+
['string', 'pathname'],
152+
['object', { name: 'a', path: 'pathname' } as Route],
153+
])(
154+
'should not redirect if the given route %s is equivalent to current location',
155+
(_, to) => {
156+
mountInRouter({ to });
157+
158+
expect(mockConsole.warn).toHaveBeenCalledWith(expect.any(String));
159+
expect(MockHistory.replace).not.toHaveBeenCalled();
160+
expect(MockHistory.push).not.toHaveBeenCalled();
161+
}
162+
);
163+
164+
it.each([
165+
['string', '/cool-page', undefined, '/cool-page'],
166+
['object', mockRoute, { id: '3' }, '/3'],
167+
])(
168+
'should use push history correctly with given route %s',
169+
(_, to, params, expected) => {
170+
mountInRouter({ to, params, push: true });
171+
expect(MockHistory.push).toHaveBeenCalledWith(expected);
172+
expect(MockHistory.replace).not.toHaveBeenCalled();
173+
}
174+
);
117175
});

src/controllers/redirect/index.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { RouterActionsType, RouterState } from '../router-store/types';
66
import { RouterSubscriber } from '../subscribers/route';
77

88
import { RedirectProps } from './types';
9+
import { generateLocationFromPath } from '../../common/utils';
910

1011
type RedirectorProps = RedirectProps & {
1112
actions: RouterActionsType;
@@ -18,8 +19,18 @@ class Redirector extends Component<RedirectorProps> {
1819
};
1920

2021
componentDidMount() {
21-
const { to, location, push, actions } = this.props;
22-
const newPath = typeof to === 'object' ? createPath(to) : to;
22+
const { to, location, push, params, query, actions } = this.props;
23+
const routeAttributes = {
24+
params,
25+
query,
26+
basePath: actions.getBasePath(),
27+
};
28+
const newPath =
29+
typeof to === 'object'
30+
? 'path' in to
31+
? createPath(generateLocationFromPath(to.path, routeAttributes))
32+
: createPath(to)
33+
: to;
2334
const currentPath = createPath(location);
2435
const action = push ? actions.push : actions.replace;
2536

src/controllers/redirect/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { Location } from '../../common/types';
1+
import { Location, MatchParams, Query, Route } from '../../common/types';
22

33
export type RedirectProps = {
4-
to: Location | string;
4+
to: Location | Route | string;
55
push?: boolean;
6+
params?: MatchParams;
7+
query?: Query;
68
};

src/index.js.flow

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import type {
77
HistoryUpdateType,
88
LinkProps,
99
Location,
10+
MatchParams,
1011
MemoryRouterProps,
12+
Query,
1113
ResourceOptions,
1214
ResourceStoreContext,
1315
Route,
@@ -32,8 +34,10 @@ export * from './types.js.flow';
3234

3335
declare export function Link(props: LinkProps): Node;
3436
declare export function Redirect(props: {
35-
to: Location | string,
37+
to: Location | Route | string,
3638
push?: boolean,
39+
params?: MatchParams,
40+
query?: Query,
3741
}): Node;
3842

3943
declare export function RouterActions(props: {|
@@ -82,7 +86,7 @@ declare export function useRouterActions(): RouterActionsType;
8286
declare export function useResourceStoreContext(): ResourceStoreContext;
8387
declare export function createRouterSelector<T, U = void>(
8488
selector: (state: RouterState, props: U) => T
85-
): (U) => T;
89+
): U => T;
8690

8791
declare export function useQueryParam(
8892
paramKey: string
@@ -149,4 +153,6 @@ type CreateResourceArg<T> =
149153
maxCache?: number,
150154
|};
151155

152-
declare export function createResource<T>(args: CreateResourceArg<T>): RouteResource<T>;
156+
declare export function createResource<T>(
157+
args: CreateResourceArg<T>
158+
): RouteResource<T>;

src/types.js.flow

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ export type Location = {|
1616

1717
export type Query = { [string]: string };
1818

19+
export type MatchParams = { [key: string]: string | null | typeof undefined };
20+
1921
export type Match = {|
2022
/** TODO we are supporting `undefined` here because we are currently using both
2123
* this version of the `Match` type, and react-routers version (which allows for `undefined`)
2224
* To fix this we should move `matchPath` to our own util so we can apply our own types, then
2325
* decide if we want to support undefined types.
2426
*/
25-
params: { [string]: string | null | typeof undefined },
27+
params: MatchParams,
2628
query: Query,
2729
isExact: boolean,
2830
path: string,
@@ -100,7 +102,9 @@ export type ResourceOptions = {
100102
routerContext?: RouterContext,
101103
};
102104

103-
export type RouteResourceUpdater<T> = (data: RouteResourceData<T>) => RouteResourceData<T>;
105+
export type RouteResourceUpdater<T> = (
106+
data: RouteResourceData<T>
107+
) => RouteResourceData<T>;
104108

105109
export type InvariantRoute = {
106110
path: string,
@@ -259,10 +263,6 @@ export type RequestResourcesParams = {
259263
timeout?: number,
260264
};
261265

262-
export type MatchParams = {
263-
[key: string]: string | null | typeof undefined,
264-
};
265-
266266
export type ResourceStoreData =
267267
| { [type: string]: RouteResourceDataForType }
268268
| {||};

src/ui/link/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const Link = forwardRef<HTMLButtonElement | HTMLAnchorElement, LinkProps>(
5555
const routeAttributes = {
5656
params,
5757
query,
58-
basePath: routerActions.getBasePath() as any,
58+
basePath: routerActions.getBasePath(),
5959
};
6060
const linkDestination =
6161
href != null

0 commit comments

Comments
 (0)