Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
inherit from route from parent, only use page override when not undef…
…ined
  • Loading branch information
joshpensky committed Sep 16, 2021
commit ea902fd4202f64618d42ecae0613516473198f79
6 changes: 3 additions & 3 deletions demos/intermediate/src/containers/Detail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,16 @@ export const beforeRouteEnter: GuardFunction<{ pokemon: SerializedPokemon }> = a
to,
from,
next,
signal,
ctx,
) => {
const { name } = to.match.params;
try {
const pokemon = await api.get(name, { signal });
const pokemon = await api.get(name, { signal: ctx.signal });
next.props({
pokemon: serializePokemon(pokemon),
});
} catch (error) {
if (!(error && error.name === 'AbortError')) {
if (error.name !== 'AbortError') {
throw new Error('Pokemon does not exist.');
}
}
Expand Down
18 changes: 13 additions & 5 deletions package/src/Guard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from 'react-router';
import { Redirect, Route } from 'react-router-dom';
import { ErrorPageContext, GuardContext, LoadingPageContext, FromRouteContext } from './contexts';
import { GuardStatus, resolveGuards } from './resolveGuards';
import { resolveGuards, ResolvedGuardStatus } from './resolveGuards';
import { useRouteChangeEffect } from './useRouteChangeEffect';
import { Meta, Page, PageComponentType } from './types';

Expand All @@ -16,7 +16,7 @@ import { Meta, Page, PageComponentType } from './types';
*
* @param page the page to type check
*/
function isPageComponentType<P>(page: Page<P>): page is PageComponentType<P> {
export function isPageComponentType<P>(page: Page<P>): page is PageComponentType<P> {
return (
!!page && typeof page !== 'string' && typeof page !== 'boolean' && typeof page !== 'number'
);
Expand All @@ -27,13 +27,16 @@ export interface GuardProps extends RouteProps {
}

export const Guard = withRouter<GuardProps & RouteComponentProps>(function GuardWithRouter({
// Guard props
children,
component,
meta,
render,
// Route component props
history,
location,
match,
staticContext,
}) {
// Track whether the component is mounted to prevent setting state after unmount
const isMountedRef = useRef(true);
Expand All @@ -45,6 +48,8 @@ export const Guard = withRouter<GuardProps & RouteComponentProps>(function Guard
}, []);

const guards = useContext(GuardContext);

type GuardStatus = { type: 'resolving' } | ResolvedGuardStatus;
function getInitialStatus(): GuardStatus {
// If there are no guards in context, the route should immediately render
if (!guards || guards.length === 0) {
Expand All @@ -58,7 +63,7 @@ export const Guard = withRouter<GuardProps & RouteComponentProps>(function Guard
// Create a mutable status variable that we can change for the *current* render
let status = immutableStatus;

const routeProps = { history, location, match };
const routeProps = { history, location, match, staticContext };
const fromRouteProps = useContext(FromRouteContext);
const routeChangeAbortControllerRef = useRef<AbortController | null>(null);
useRouteChangeEffect(routeProps, async () => {
Expand All @@ -84,9 +89,12 @@ export const Guard = withRouter<GuardProps & RouteComponentProps>(function Guard
try {
// Resolve the guards to get the render status
const status = await resolveGuards(guards || [], {
to: { ...routeProps, meta: meta || {} },
to: routeProps,
from: fromRouteProps,
signal: abortController.signal,
context: {
meta: meta || {},
signal: abortController.signal,
},
});
// If the signal hasn't been aborted, set the new status!
if (isMountedRef.current && !abortController.signal.aborted) {
Expand Down
24 changes: 18 additions & 6 deletions package/src/GuardProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@ export type GuardProviderProps = BaseGuardProps;

export const GuardProvider = withRouter<GuardProviderProps & RouteComponentProps>(
function GuardProviderWithRouter({
// Guard provider props
children,
guards,
ignoreGlobal,
loading,
error,
loading: loadingPageOverride,
error: errorPageOverride,
// Route component props
history,
location,
match,
staticContext,
}) {
const routeProps = { history, location, match };
const routeProps = { history, location, match, staticContext };
const fromRouteProps = useRouteChangeEffect(routeProps, () => {});
const parentFromRouteProps = useContext(FromRouteContext);

const providerGuards = useGlobalGuards(guards, ignoreGlobal);

Expand All @@ -28,9 +32,17 @@ export const GuardProvider = withRouter<GuardProviderProps & RouteComponentProps

return (
<GuardContext.Provider value={providerGuards}>
<LoadingPageContext.Provider value={loading || loadingPage}>
<ErrorPageContext.Provider value={error || errorPage}>
<FromRouteContext.Provider value={fromRouteProps}>{children}</FromRouteContext.Provider>
<LoadingPageContext.Provider
value={typeof loadingPageOverride !== 'undefined' ? loadingPageOverride : loadingPage}>
<ErrorPageContext.Provider
value={typeof errorPageOverride !== 'undefined' ? errorPageOverride : errorPage}>
{/**
* Prioritize the parent FromRoute props over the child (which uses the closest Route's match)
* https://reactrouter.com/web/api/withRouter
*/}
<FromRouteContext.Provider value={parentFromRouteProps || fromRouteProps}>
{children}
</FromRouteContext.Provider>
</ErrorPageContext.Provider>
</LoadingPageContext.Provider>
</GuardContext.Provider>
Expand Down
10 changes: 6 additions & 4 deletions package/src/GuardedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ export interface GuardedRouteProps extends BaseGuardProps, RouteProps {
export const GuardedRoute: React.FunctionComponent<GuardedRouteProps> = ({
children,
component,
error,
error: errorPageOverride,
guards,
ignoreGlobal,
loading,
loading: loadingPageOverride,
meta,
render,
path,
Expand All @@ -33,8 +33,10 @@ export const GuardedRoute: React.FunctionComponent<GuardedRouteProps> = ({
return (
<Route path={path} {...routeProps}>
<GuardContext.Provider value={routeGuards}>
<LoadingPageContext.Provider value={loading || loadingPage}>
<ErrorPageContext.Provider value={error || errorPage}>
<LoadingPageContext.Provider
value={typeof loadingPageOverride !== 'undefined' ? loadingPageOverride : loadingPage}>
<ErrorPageContext.Provider
value={typeof errorPageOverride !== 'undefined' ? errorPageOverride : errorPage}>
<Guard path={path} meta={meta} component={component} render={render}>
{children}
</Guard>
Expand Down
2 changes: 1 addition & 1 deletion package/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export { GuardedRoute, GuardedRouteProps } from './GuardedRoute';
export {
BaseGuardProps,
GuardFunction,
Next,
NextFunction,
Page,
LoadingPageComponentType,
ErrorPageComponentType,
Expand Down
18 changes: 8 additions & 10 deletions package/src/resolveGuards.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import { RouteComponentProps } from 'react-router';
import {
GuardFunction,
Next,
NextFunction,
NextAction,
NextPropsPayload,
NextRedirectPayload,
GuardToRoute,
GuardFunctionContext,
} from './types';

export type ResolvedGuardStatus =
| { type: 'error'; error: unknown }
| { type: 'redirect'; redirect: NextRedirectPayload }
| { type: 'render'; props: NextPropsPayload };

export type GuardStatus = { type: 'resolving' } | ResolvedGuardStatus;

export interface ResolveGuardsContext {
to: GuardToRoute;
to: RouteComponentProps<Record<string, string>>;
from: RouteComponentProps<Record<string, string>> | null;
signal: AbortSignal;
context: GuardFunctionContext;
}

const NextFunctionFactory = {
export const NextFunctionFactory = {
/**
* Builds a new next function using the given `resolve` callback.
*/
build: (resolve: (action: NextAction) => void): Next<{}> => {
build: (resolve: (action: NextAction) => void): NextFunction<{}> => {
function next() {
resolve({ type: 'continue' });
}
Expand All @@ -49,10 +47,10 @@ const NextFunctionFactory = {
* @param context the context of this guard's resolution
* @returns a Promise returning the resolved guard action
*/
function runGuard(guard: GuardFunction, context: ResolveGuardsContext): Promise<NextAction> {
export function runGuard(guard: GuardFunction, context: ResolveGuardsContext): Promise<NextAction> {
return new Promise<NextAction>(async (resolve, reject) => {
try {
await guard(context.to, context.from, NextFunctionFactory.build(resolve), context.signal);
await guard(context.to, context.from, NextFunctionFactory.build(resolve), context.context);
} catch (error) {
reject(error);
}
Expand Down
35 changes: 26 additions & 9 deletions package/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,44 @@ export interface NextRedirectAction {

export type NextAction = NextContinueAction | NextPropsAction | NextRedirectAction;

export interface Next<Props extends {}> {
export interface NextFunction<Props extends {}> {
/** Resolve the guard and continue to the next, if any. */
(): void;
/** Pass the props to the resolved route and continue to the next, if any. */
props(props: Props): void;
/** Redirect to the given route. */
redirect(to: LocationDescriptor): void;
}

///////////////////////////////
// Guards
///////////////////////////////
export type GuardFunctionRouteProps = RouteComponentProps<Record<string, any>>;
export type GuardToRoute = GuardFunctionRouteProps & {
export interface GuardFunctionContext {
/** Metadata attached on the `to` route. */
meta: Meta;
};
/**
* A signal that determines if the current guard resolution has been aborted.
* Attach to fetch calls to cancel outdated requests before they're resolved.
*/
signal: AbortSignal;
}

export type GuardFunction<Props extends {} = {}> = (
to: GuardToRoute,
from: GuardFunctionRouteProps | null,
next: Next<Props>,
signal: AbortSignal,
/** The route being navigated to. */
to: RouteComponentProps<Record<string, any>>,
/** The route being navigated from, if any */
from: RouteComponentProps<Record<string, any>> | null,
/** The guard's next function */
next: NextFunction<Props>,
/** Context for this guard's execution */
context: GuardFunctionContext,
) => void;

///////////////////////////////
// Page Types
///////////////////////////////
export type PageComponentType<P = {}> = ComponentType<RouteComponentProps & P>;
export type Page<P = {}> = PageComponentType<P> | null | undefined | string | boolean | number;
export type Page<P = {}> = PageComponentType<P> | null | string | boolean | number;

export type LoadingPage = Page;
export type ErrorPage = Page<{ error: unknown }>;
Expand All @@ -64,8 +77,12 @@ export type ErrorPageComponentType = PageComponentType<{ error: unknown }>;
// Props
///////////////////////////////
export interface BaseGuardProps {
/** Guards to attach as middleware. */
guards?: GuardFunction[];
/** Whether to ignore guards attached to parent providers. */
ignoreGlobal?: boolean;
/** A custom loading page component. */
loading?: LoadingPage;
/** A custom error page component. */
error?: ErrorPage;
}