From 35cf0e124c8c4b2562338cd69b5b9686340cac5a Mon Sep 17 00:00:00 2001 From: Yehor Hornostaiev Date: Mon, 1 Nov 2021 13:27:43 +0200 Subject: [PATCH] routeState: initial --- examples/routing-with-route-state/index.html | 13 ++++++++ examples/routing-with-route-state/index.tsx | 29 +++++++++++++++++ examples/routing-with-route-state/routes.tsx | 31 +++++++++++++++++++ .../state-consumer-with-redirection.tsx | 14 +++++++++ .../state-consumer.tsx | 17 ++++++++++ .../state-provider.tsx | 29 +++++++++++++++++ src/common/types.ts | 16 +++++----- src/common/utils/generate-location/index.ts | 3 +- src/common/utils/history/index.ts | 20 ++++++------ src/common/utils/router-context/index.ts | 2 +- src/controllers/hooks/router-store/index.tsx | 12 +++---- src/controllers/redirect/index.tsx | 3 +- src/controllers/router-store/index.tsx | 8 ++--- src/controllers/router-store/types.ts | 4 +-- src/ui/link/index.tsx | 24 ++++++++------ src/ui/link/utils/handle-navigation.tsx | 14 +++++---- 16 files changed, 192 insertions(+), 47 deletions(-) create mode 100644 examples/routing-with-route-state/index.html create mode 100644 examples/routing-with-route-state/index.tsx create mode 100644 examples/routing-with-route-state/routes.tsx create mode 100644 examples/routing-with-route-state/state-consumer-with-redirection.tsx create mode 100644 examples/routing-with-route-state/state-consumer.tsx create mode 100644 examples/routing-with-route-state/state-provider.tsx 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); } } };