diff --git a/examples/routing-with-route-state/index.html b/examples/routing-with-route-state/index.html
new file mode 100644
index 00000000..676ae0a1
--- /dev/null
+++ b/examples/routing-with-route-state/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Example - Routing with route state
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/routing-with-route-state/index.tsx b/examples/routing-with-route-state/index.tsx
new file mode 100644
index 00000000..a86bb9e9
--- /dev/null
+++ b/examples/routing-with-route-state/index.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import {
+ Router,
+ RouteComponent,
+ createBrowserHistory,
+} from 'react-resource-router';
+
+import { stateProviderRoute, stateConsumerRoute, stateConsumerWithRedirectionRoute } from './routes';
+
+const myHistory = createBrowserHistory();
+
+const appRoutes = [stateProviderRoute, stateConsumerRoute, stateConsumerWithRedirectionRoute];
+
+const App = () => {
+ return (
+ console.log('Prefetcing route', route.name)}
+ >
+
+
+ );
+};
+
+ReactDOM.render(, document.getElementById('root'));
diff --git a/examples/routing-with-route-state/routes.tsx b/examples/routing-with-route-state/routes.tsx
new file mode 100644
index 00000000..d1b41c1f
--- /dev/null
+++ b/examples/routing-with-route-state/routes.tsx
@@ -0,0 +1,31 @@
+import { StateConsumer } from './state-consumer';
+import { StateProvider } from './state-provider';
+import { StateConsumerWithRedirection } from './state-consumer-with-redirection';
+
+export const stateProviderRoute = {
+ name: 'provider',
+ path: '/',
+ exact: true,
+ component: StateProvider,
+ navigation: null,
+ resources: [],
+};
+
+export const stateConsumerRoute = {
+ name: 'consumer',
+ path: '/consumer',
+ exact: true,
+ component: StateConsumer,
+ navigation: null,
+ resources: [],
+};
+
+export const stateConsumerWithRedirectionRoute = {
+ name: 'consumer-with-redirection',
+ path: '/redirector',
+ exact: true,
+ component: StateConsumerWithRedirection,
+ navigation: null,
+ resources: [],
+};
+
diff --git a/examples/routing-with-route-state/state-consumer-with-redirection.tsx b/examples/routing-with-route-state/state-consumer-with-redirection.tsx
new file mode 100644
index 00000000..429710fb
--- /dev/null
+++ b/examples/routing-with-route-state/state-consumer-with-redirection.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import {
+ Redirect
+} from 'react-resource-router';
+
+
+export const StateConsumerWithRedirection = () => {
+ return (
+ ' }
+ }} />
+ );
+};
diff --git a/examples/routing-with-route-state/state-consumer.tsx b/examples/routing-with-route-state/state-consumer.tsx
new file mode 100644
index 00000000..26d916cf
--- /dev/null
+++ b/examples/routing-with-route-state/state-consumer.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import {
+ useRouter
+} from 'react-resource-router';
+
+
+export const StateConsumer = () => {
+ const [{ location: { state = {} } }, { goBack, push }] = useRouter();
+ const { message, replaced } = state as any;
+
+ return (
+ <>
+
+ {message}
+ >
+ );
+};
diff --git a/examples/routing-with-route-state/state-provider.tsx b/examples/routing-with-route-state/state-provider.tsx
new file mode 100644
index 00000000..806da360
--- /dev/null
+++ b/examples/routing-with-route-state/state-provider.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import {
+ Link,
+ useRouterActions
+} from 'react-resource-router';
+
+
+export const StateProvider = () => {
+ const { push, replace } = useRouterActions();
+ return (
+ <>
+ There is a several variants presented to pass some state options to the next route
+
+
+
+
+ ' }
+ }}>Use LINK component
+
+ Use LINK to REDIRECT component
+ >
+ );
+};
diff --git a/src/common/types.ts b/src/common/types.ts
index e9230719..096e97f3 100644
--- a/src/common/types.ts
+++ b/src/common/types.ts
@@ -8,7 +8,7 @@ import {
AnchorHTMLAttributes,
} from 'react';
-import { History, Location as HistoryLocationShape } from 'history';
+import { History, Location as HistoryLocationShape, LocationState } from 'history';
export type LocationShape = HistoryLocationShape;
@@ -16,8 +16,9 @@ export type Href = string;
export type Location = {
pathname: string;
- search: string;
- hash: string;
+ search?: string;
+ hash?: string;
+ state?: LocationState;
};
export type BrowserHistory = Omit<
@@ -25,8 +26,8 @@ export type BrowserHistory = Omit<
'location' | 'go' | 'createHref' | 'push' | 'replace'
> & {
location: Location;
- push: (path: string) => void;
- replace: (path: string) => void;
+ push: (path: string, state?: unknown) => void;
+ replace: (path: string, state?: unknown) => void;
};
export type MatchParams = {
@@ -109,7 +110,7 @@ export type RouteResourceResponseLoaded = {
export type RouteResourceResponse<
RouteResourceData = unknown
-> = RouteResourceResponseBase &
+ > = RouteResourceResponseBase &
(
| RouteResourceResponseLoading
| RouteResourceResponseError
@@ -237,7 +238,7 @@ export type LinkProps = AnchorHTMLAttributes & {
children: ReactNode;
target?: '_blank' | '_self' | '_parent' | '_top';
href?: string;
- to?: string | Route | Promise<{ default: Route } | Route>;
+ to?: string | Route | Location | Promise<{ default: Route }>;
replace?: boolean;
type?: 'a' | 'button';
onClick?: (e: MouseEvent | KeyboardEvent) => void;
@@ -291,6 +292,7 @@ export type GenerateLocationOptions = {
params?: MatchParams;
query?: Query;
basePath?: string;
+ state?: LocationState;
};
export type CreateRouterContextOptions = {
diff --git a/src/common/utils/generate-location/index.ts b/src/common/utils/generate-location/index.ts
index beb4fb84..6cc690a3 100644
--- a/src/common/utils/generate-location/index.ts
+++ b/src/common/utils/generate-location/index.ts
@@ -7,7 +7,7 @@ export function generateLocationFromPath(
pattern = '/',
options: GenerateLocationOptions = {}
): Location {
- const { params = {}, query = {}, basePath = '' } = options;
+ const { params = {}, query = {}, basePath = '', state } = options;
// @ts-ignore stringify accepts two params but it's type doesn't say so
const stringifiedQuery = qs.stringify(query, true);
const pathname =
@@ -17,5 +17,6 @@ export function generateLocationFromPath(
pathname: `${basePath}${pathname}`,
search: stringifiedQuery,
hash: '',
+ state
};
}
diff --git a/src/common/utils/history/index.ts b/src/common/utils/history/index.ts
index 8658df80..02e82b6f 100644
--- a/src/common/utils/history/index.ts
+++ b/src/common/utils/history/index.ts
@@ -18,7 +18,7 @@ const methodsPlaceholders = {
block: () => noop,
};
-const getLocation = () => {
+const getLocation = (): Location => {
// todo - don't force non-optional search and hash
const { pathname = '', search = '', hash = '' } =
(hasWindow() && window.location) || {};
@@ -107,15 +107,15 @@ export const createLegacyHistory = (): BrowserHistory => {
},
...(hasWindow()
? {
- push: (path: string) => window.location.assign(path),
- replace: (path: string) =>
- window.history.replaceState({}, document.title, path),
- goBack: () => window.history.back(),
- goForward: () => window.history.forward(),
- listen: createLegacyListener(updateExposedLocation),
- block: () => noop,
- createHref: (location: Location) => createPath(location),
- }
+ push: (path: string) => window.location.assign(path),
+ replace: (path: string) =>
+ window.history.replaceState({}, document.title, path),
+ goBack: () => window.history.back(),
+ goForward: () => window.history.forward(),
+ listen: createLegacyListener(updateExposedLocation),
+ block: () => noop,
+ createHref: (location: Location) => createPath(location),
+ }
: methodsPlaceholders),
};
};
diff --git a/src/common/utils/router-context/index.ts b/src/common/utils/router-context/index.ts
index 2f9bb10d..a8d00bcf 100644
--- a/src/common/utils/router-context/index.ts
+++ b/src/common/utils/router-context/index.ts
@@ -33,7 +33,7 @@ export const findRouterContext = (
): RouterContext => {
const { location, basePath = '' } = options;
const { pathname, search } = location;
- const query = qs.parse(search) as Query;
+ const query = search ? qs.parse(search) as Query : {};
const matchedRoute = matchRoute(routes, pathname, query, basePath);
return {
diff --git a/src/controllers/hooks/router-store/index.tsx b/src/controllers/hooks/router-store/index.tsx
index e6af562d..d14f9262 100644
--- a/src/controllers/hooks/router-store/index.tsx
+++ b/src/controllers/hooks/router-store/index.tsx
@@ -56,9 +56,9 @@ const createPathParamHook = createHook<
export const useQueryParam = (
paramKey: string
): [
- string | undefined,
- (newValue: string | undefined, updateType?: HistoryUpdateType) => void
-] => {
+ string | undefined,
+ (newValue: string | undefined, updateType?: HistoryUpdateType) => void
+ ] => {
const [paramVal, routerActions] = createQueryParamHook({ paramKey });
const setQueryParam = React.useCallback(
@@ -77,9 +77,9 @@ export const useQueryParam = (
export const usePathParam = (
paramKey: string
): [
- string | undefined,
- (newValue: string | undefined, updateType?: HistoryUpdateType) => void
-] => {
+ string | undefined,
+ (newValue: string | undefined, updateType?: HistoryUpdateType) => void
+ ] => {
const [paramVal, routerActions] = createPathParamHook({ paramKey });
const setPathParam = React.useCallback(
diff --git a/src/controllers/redirect/index.tsx b/src/controllers/redirect/index.tsx
index 593052e1..9e9b13e2 100644
--- a/src/controllers/redirect/index.tsx
+++ b/src/controllers/redirect/index.tsx
@@ -22,6 +22,7 @@ class Redirector extends Component {
const newPath = typeof to === 'object' ? createPath(to) : to;
const currentPath = createPath(location);
const action = push ? actions.push : actions.replace;
+ const state = to && typeof to !== 'string' ? to.state : undefined;
if (currentPath === newPath) {
if (
@@ -37,7 +38,7 @@ class Redirector extends Component {
return;
}
- action(newPath);
+ action(newPath, state);
}
render() {
diff --git a/src/controllers/router-store/index.tsx b/src/controllers/router-store/index.tsx
index 14d0e1f6..32c37ad9 100644
--- a/src/controllers/router-store/index.tsx
+++ b/src/controllers/router-store/index.tsx
@@ -228,12 +228,12 @@ const actions: AllRouterActions = {
});
},
- push: path => ({ getState }) => {
+ push: (path, state) => ({ getState }) => {
const { history, basePath } = getState();
if (isExternalAbsolutePath(path)) {
window.location.assign(path as string);
} else {
- history.push(getRelativePath(path, basePath) as any);
+ history.push(getRelativePath(path, basePath) as any, state);
}
},
@@ -247,12 +247,12 @@ const actions: AllRouterActions = {
history.push(location as any);
},
- replace: path => ({ getState }) => {
+ replace: (path, state) => ({ getState }) => {
const { history, basePath } = getState();
if (isExternalAbsolutePath(path)) {
window.location.replace(path as string);
} else {
- history.replace(getRelativePath(path, basePath) as any);
+ history.replace(getRelativePath(path, basePath) as any, state);
}
},
diff --git a/src/controllers/router-store/types.ts b/src/controllers/router-store/types.ts
index 6c041a92..5d6d069e 100644
--- a/src/controllers/router-store/types.ts
+++ b/src/controllers/router-store/types.ts
@@ -103,9 +103,9 @@ type PrivateRouterActions = {
};
type PublicRouterActions = {
- push: (path: Href, state?: any) => RouterAction;
+ push: (path: Href, state?: unknown) => RouterAction;
pushTo: (route: Route, attributes?: ToAttributes) => RouterAction;
- replace: (path: Href) => RouterAction;
+ replace: (path: Href, state?: unknown) => RouterAction;
replaceTo: (route: Route, attributes?: ToAttributes) => RouterAction;
goBack: () => RouterAction;
goForward: () => RouterAction;
diff --git a/src/ui/link/index.tsx b/src/ui/link/index.tsx
index 69eed99e..1fe7a664 100644
--- a/src/ui/link/index.tsx
+++ b/src/ui/link/index.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import { createPath } from 'history';
import {
createElement,
@@ -10,7 +11,7 @@ import {
KeyboardEvent,
} from 'react';
-import { LinkProps, Route } from '../../common/types';
+import { LinkProps, Route, Location } from '../../common/types';
import {
createRouterContext,
generateLocationFromPath,
@@ -44,11 +45,13 @@ const Link = forwardRef(
const prefetchRef = useRef();
const validLinkType = getValidLinkType(linkType);
- const [route, setRoute] = useState(() => {
+ const [route, setRoute] = useState(() => {
if (to && typeof to !== 'string') {
- if ('then' in to)
+ if ('then' in to) {
to.then(r => setRoute('default' in r ? r.default : r));
- else return to;
+ }
+
+ return to as Route;
}
});
@@ -57,22 +60,24 @@ const Link = forwardRef(
query,
basePath: routerActions.getBasePath() as any,
};
+
const linkDestination =
href != null
? href
: typeof to !== 'string'
- ? (route &&
+ ? (route &&
createPath(
- generateLocationFromPath(route.path, routeAttributes)
+ 'pathname' in route ? route :
+ generateLocationFromPath(route.path, routeAttributes)
)) ||
''
- : to;
+ : to;
const triggerPrefetch = useCallback(() => {
prefetchRef.current = undefined;
// ignore if async route not ready yet
- if (typeof to !== 'string' && !route) return;
+ if (typeof to !== 'string' && !route || route && 'pathname' in route) return;
const context =
typeof to !== 'string' && route
@@ -98,7 +103,8 @@ const Link = forwardRef(
replace,
routerActions,
href: linkDestination,
- to: route && [route, { params, query }],
+ to: route && 'component' in route ? [route, { params, query }] : undefined,
+ state: route && 'pathname' in route ? route.state : undefined
});
const handleMouseEnter = (e: MouseEvent) => {
diff --git a/src/ui/link/utils/handle-navigation.tsx b/src/ui/link/utils/handle-navigation.tsx
index 64592377..ec8fc4cb 100644
--- a/src/ui/link/utils/handle-navigation.tsx
+++ b/src/ui/link/utils/handle-navigation.tsx
@@ -1,5 +1,6 @@
import { KeyboardEvent, MouseEvent } from 'react';
-import { Route } from '../../../common/types';
+import { LocationState } from 'history';
+import { Route, Location } from '../../../common/types';
import { isKeyboardEvent, isModifiedEvent } from '../../../common/utils/event';
@@ -8,20 +9,21 @@ type LinkNavigationEvent = MouseEvent | KeyboardEvent;
type LinkPressArgs = {
target?: string;
routerActions: {
- push: (href: string) => void;
- replace: (href: string) => void;
+ push: (href: string, state?: LocationState) => void;
+ replace: (href: string, state?: LocationState) => void;
pushTo: (route: Route, attributes: any) => void;
replaceTo: (route: Route, attributes: any) => void;
};
replace: boolean;
href: string;
onClick?: (e: LinkNavigationEvent) => void;
- to: [Route, any] | void;
+ to: [Route, any] | undefined;
+ state?: LocationState;
};
export const handleNavigation = (
event: any,
- { onClick, target, replace, routerActions, href, to }: LinkPressArgs
+ { onClick, target, replace, routerActions, href, to, state }: LinkPressArgs
): void => {
if (isKeyboardEvent(event) && event.keyCode !== 13) {
return;
@@ -41,7 +43,7 @@ export const handleNavigation = (
method(...to);
} else {
const method = replace ? routerActions.replace : routerActions.push;
- method(href);
+ method(href, state);
}
}
};