Skip to content

Commit

Permalink
[react-router-dom] update Route component type (#4418)
Browse files Browse the repository at this point in the history
- Add missing props to Route component.
- Import types from source code then adapt to flow.
- update and fix Route component tests according to the changes.
  • Loading branch information
iamdey committed Feb 6, 2023
1 parent b6ecf2d commit d2cf939
Show file tree
Hide file tree
Showing 2 changed files with 329 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,271 @@ declare module "react-router-dom" {
children?: React$Node
|}>

declare export var Route: React$ComponentType<{|
caseSensitive?: boolean,
children?: React$Node,
element?: React$Element<any> | null,
index?: boolean,
path?: string,
|}>
declare export type ResultTypeData = 'data'
declare export type ResultTypeDeferred = 'deferred'
declare export type ResultTypeRedirect = 'redirect'
declare export type ResultTypeError = 'error'

/**
* Successful result from a loader or action
*/
declare export type SuccessResult = {|
type: ResultTypeData;
data: any;
statusCode?: number;
headers?: Headers;
|}

declare export class DeferredData {
(data: mixed, responseInit?: ResponseOptions): void;
subscribe: mixed,
cancel: mixed,
resolveData: mixed,
done: mixed,
unwrappedData: mixed,
pendingKeys: mixed,
}

/**
* Successful defer() result from a loader or action
*/
declare export type DeferredResult = {|
type: ResultTypeDeferred;
deferredData: DeferredData;
statusCode?: number;
headers?: Headers;
|}

/**
* Redirect result from a loader or action
*/
declare export type RedirectResult = {|
type: ResultTypeRedirect;
status: number;
location: string;
revalidate: boolean;
|}

/**
* Unsuccessful result from a loader or action
*/
declare export type ErrorResult = {|
type: ResultTypeError;
error: any;
headers?: Headers;
|}

/**
* Result from a loader or action - potentially successful or unsuccessful
*/
declare export type DataResult =
| SuccessResult
| DeferredResult
| RedirectResult
| ErrorResult;

declare export type MutationFormMethod = "post" | "put" | "patch" | "delete";
declare export type FormMethod = "get" | MutationFormMethod;
declare export type FormEncType =
| "application/x-www-form-urlencoded"
| "multipart/form-data";

/**
* @private
* Internal interface to pass around for action submissions, not intended for
* external consumption
*/
declare export type Submission = {|
formMethod: FormMethod;
formAction: string;
formEncType: FormEncType;
formData: FormData;
|}

/**
* Index routes must not have children
*/
declare export type AgnosticIndexRouteObject = {|
...AgnosticBaseRouteObject,
children?: void;
index: true;
|};

declare export type AgnosticDataIndexRouteObject = {|
...AgnosticIndexRouteObject,
id: string;
|};

declare export type AgnosticNonIndexRouteObject = {|
...AgnosticBaseRouteObject,
children?: AgnosticRouteObject[];
index?: false;
|};

declare export type AgnosticDataNonIndexRouteObject = {|
...AgnosticNonIndexRouteObject,
children?: AgnosticDataRouteObject[];
id: string;
|};

/**
* A data route object, which is just a RouteObject with a required unique ID
*/
declare export type AgnosticDataRouteObject =
| AgnosticDataIndexRouteObject
| AgnosticDataNonIndexRouteObject;

/**
* A route object represents a logical route, with (optionally) its child
* routes organized in a tree-like structure.
*/
declare export type AgnosticRouteObject =
| AgnosticIndexRouteObject
| AgnosticNonIndexRouteObject;

/**
* A RouteMatch contains info about how a route matched a URL.
*/
declare export type AgnosticRouteMatch<
ParamKey: string = string,
RouteObjectType: AgnosticRouteObject = AgnosticRouteObject
> = {|
/**
* The names and values of dynamic parameters in the URL.
*/
params: Params<ParamKey>;
/**
* The portion of the URL pathname that was matched.
*/
pathname: string;
/**
* The portion of the URL pathname that was matched before child routes.
*/
pathnameBase: string;
/**
* The route object that was used to match.
*/
route: RouteObjectType;
|}

declare export type AgnosticDataRouteMatch = AgnosticRouteMatch<string, AgnosticDataRouteObject>;

/**
* @private
* Arguments passed to route loader/action functions. Same for now but we keep
* this as a private implementation detail in case they diverge in the future.
*/
declare type DataFunctionArgs = {|
request: Request;
params: Params<string>;
context?: any;
|}

/**
* Arguments passed to loader functions
*/
declare export type LoaderFunctionArgs = DataFunctionArgs;


/**
* Arguments passed to action functions
*/
declare export type ActionFunctionArgs = DataFunctionArgs;

/**
* Route loader function signature
*/
declare export type LoaderFunction = (args: LoaderFunctionArgs) => Promise<Response> | Response | Promise<any> | any;

/**
* Route action function signature
*/
declare export type ActionFunction = (args: ActionFunctionArgs) => Promise<Response> | Response | Promise<any> | any;

/**
* Route shouldRevalidate function signature. This runs after any submission
* (navigation or fetcher), so we flatten the navigation/fetcher submission
* onto the arguments. It shouldn't matter whether it came from a navigation
* or a fetcher, what really matters is the URLs and the formData since loaders
* have to re-run based on the data models that were potentially mutated.
*/
declare export type ShouldRevalidateFunction = (args: {|
currentUrl: URL;
currentParams: AgnosticDataRouteMatch["params"];
nextUrl: URL;
nextParams: AgnosticDataRouteMatch["params"];
formMethod?: Submission["formMethod"];
formAction?: Submission["formAction"];
formEncType?: Submission["formEncType"];
formData?: Submission["formData"];
actionResult?: DataResult;
defaultShouldRevalidate: boolean;
|}) => boolean;

/**
* Base RouteObject with common props shared by all types of routes
*/
declare type AgnosticBaseRouteObject = {|
caseSensitive?: boolean;
path?: string;
id?: string;
loader?: LoaderFunction;
action?: ActionFunction;
hasErrorBoundary?: boolean;
shouldRevalidate?: ShouldRevalidateFunction;
handle?: mixed;
|};

declare export type NonIndexRouteObject = {|
caseSensitive?: AgnosticNonIndexRouteObject["caseSensitive"];
path?: AgnosticNonIndexRouteObject["path"];
id?: AgnosticNonIndexRouteObject["id"];
loader?: AgnosticNonIndexRouteObject["loader"];
action?: AgnosticNonIndexRouteObject["action"];
hasErrorBoundary?: AgnosticNonIndexRouteObject["hasErrorBoundary"];
shouldRevalidate?: AgnosticNonIndexRouteObject["shouldRevalidate"];
handle?: AgnosticNonIndexRouteObject["handle"];
index?: false;
children?: RouteObject[];
element?: React$Node | null;
errorElement?: React$Node | null;
|}

declare export type PathRouteProps = {|
caseSensitive?: NonIndexRouteObject["caseSensitive"];
path?: NonIndexRouteObject["path"];
id?: NonIndexRouteObject["id"];
loader?: NonIndexRouteObject["loader"];
action?: NonIndexRouteObject["action"];
hasErrorBoundary?: NonIndexRouteObject["hasErrorBoundary"];
shouldRevalidate?: NonIndexRouteObject["shouldRevalidate"];
handle?: NonIndexRouteObject["handle"];
index?: false;
children?: React$Node;
element?: React$Node | null;
errorElement?: React$Node | null;
|}

declare export type LayoutRouteProps = PathRouteProps

declare export type IndexRouteProps = {|
caseSensitive?: IndexRouteObject["caseSensitive"];
path?: IndexRouteObject["path"];
id?: IndexRouteObject["id"];
loader?: IndexRouteObject["loader"];
action?: IndexRouteObject["action"];
hasErrorBoundary?: IndexRouteObject["hasErrorBoundary"];
shouldRevalidate?: IndexRouteObject["shouldRevalidate"];
handle?: IndexRouteObject["handle"];
index: true;
children?: void;
element?: React$Node | null;
errorElement?: React$Node | null;
|}

declare export type RouteProps = PathRouteProps | LayoutRouteProps | IndexRouteProps;

declare export var Route: React$ComponentType<RouteProps>

declare export var Prompt: React$ComponentType<{|
message: string | ((location: Location) => string | boolean),
Expand Down Expand Up @@ -189,13 +447,22 @@ declare module "react-router-dom" {

declare export function generatePath(pattern?: string, params?: { +[string]: mixed, ... }): string;

declare type RouteObject = {|
caseSensitive?: boolean,
children?: Array<RouteObject>,
element?: React$Node,
index?: boolean,
path?: string,
|};
declare export type IndexRouteObject = {|
caseSensitive?: AgnosticIndexRouteObject["caseSensitive"];
path?: AgnosticIndexRouteObject["path"];
id?: AgnosticIndexRouteObject["id"];
loader?: AgnosticIndexRouteObject["loader"];
action?: AgnosticIndexRouteObject["action"];
hasErrorBoundary?: AgnosticIndexRouteObject["hasErrorBoundary"];
shouldRevalidate?: AgnosticIndexRouteObject["shouldRevalidate"];
handle?: AgnosticIndexRouteObject["handle"];
index: true;
children?: void;
element?: React$Node | null;
errorElement?: React$Node | null;
|}

declare export type RouteObject = IndexRouteObject | NonIndexRouteObject;

declare export function createRoutesFromChildren(
children: React$Node,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,14 +408,61 @@ describe("react-router-dom", () => {
index
caseSensitive
/>;

<Route><div>Hi!</div></Route>;

<Route
caseSensitive
path="/login"
id="login"
loader={({ request, params }) => {
const myRequest: Request = request;
const myParams: Params<string> = params;
}}
action={() => {}}
hasErrorBoundary
shouldRevalidate={() => false}
handle={{ breadcrumb: 'login'}}
index={false}
element={<Component />}
errorElement={<Component />}
/>;
});

it("raises error if passed incorrect props", () => {
// $FlowExpectedError[incompatible-type] - prop must be a string
<Route path={123} />;

// $FlowExpectedError[prop-missing] - unexpected prop xxx
// $FlowExpectedError[incompatible-type] - unexpected prop xxx
<Route xxx="1" />;

// $FlowExpectedError[incompatible-type]
<Route action={(invalid: number) => true} />;

// $FlowExpectedError[incompatible-type]
<Route caseSensitive="123" />;

<Route loader={({ request, params, ...loaderArgs}) => {
// $FlowExpectedError[incompatible-type]
const myRequest: string = request;

// $FlowExpectedError[incompatible-type]
const myParams: string = params;

// $FlowExpectedError[prop-missing]
const missing: any = loaderArgs.missing;

return false;
}} />;

// $FlowExpectedError[incompatible-type]
<Route hasErrorBoundary="invalid" />;

// $FlowExpectedError[incompatible-type]
<Route shouldRevalidate={({ currentUrl }: {| currentUrl: number |}) => true} />;

// $FlowExpectedError[incompatible-type]
<Route index="invalid" />;
});
});

Expand Down

0 comments on commit d2cf939

Please sign in to comment.