This is kind of a continuation of my first issue I created here. It is, again, more of an issue with "how do I use TypeScript correctly" but I think it would great to have some help regarding these issues in the Reach Router documentation because they are not very intuitive to solve with only moderate TypeScript experience.
Let's say I have a Page component called TodoListPage:
interface Props extends RouteComponentProps {
todoListId: string;
}
class TodoListPage extends React.Component<Props, any> {}
I'm expecting to get the todoListId prop from the router:
<TodoListPage path="/todo-lists/:todoListId" />
When writing the above code, the TypeScript compiler is angry because the required prop todoListId is obviously not defined (check the last line of the error):
Type '{ path: string; }' is not assignable to type 'IntrinsicAttributes & Props & { children?: ReactNode; }'.
Type '{ path: string; }' is not assignable to type 'Props'.
Property 'todoListId' is missing in type '{ path: string; }'.
It makes sense because the component is missing the required prop and only Reach Router will eventually pass it to the component when the user navigates to the page.
How do I make this clear to TypeScript? What is the recommended approach to get around this issue?
I'm also looking for options here, I ended up with setting defaults like this:
<TodoListPage path="/todo-lists/:todoListId" todoListId="default-id" />
You can define prop as optional:
todoListId?: string;
@iksz1 yes - but then you need to check the todoListId inside the component, because ts complains that the todoListId possibly might be not defined
I have same problem for almost all of my components in Route component
My Code:
<Router>
<App path="/" />
<Login path="/login" />
<NotFound default={true} />
</Router>
Error Message:
error TS2339: Property 'default' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes
& Readonly<{
children?: ReactNode; }> & Readonly<{}>'.
TypeScript Version: 3.0.3
React: 16.5
@reach/router: 1.1.1
Issues in Reason as well. Component api designed around React.cloneElement is usually hostile to static typing.
Try this:
export interface Props extends Router.RouteComponentProps<{
todoListId: string
}> {}
I am having issues with setting the path prop on components wrapped with the Redux HOC, in which case adding a prop to the wrapped component just didn't do it for me.
Do you have any suggestions for integrating Redux containers and Reach routes?
Any update on this guys?
I was considering using Reach Router but without proper support for TypeScript, it' a no go.
I have an app that uses redux and we are using material-ui
in our index.tsx file we have this
import * as React from "react"
import * as ReactDOM from "react-dom"
import { Provider } from "react-redux"
import { MuiThemeProvider, createMuiTheme } from "@material-ui/core/styles"
import AxiosContentService from "@services/AxiosContentService"
import { initialStoreState } from "./reducers"
import configureStore from "./store"
import Routes from "./routes"
const store = configureStore(initialStoreState)
AxiosContentService.initializeAxios(store)
const theme = createMuiTheme({
typography: {
useNextVariants: true,
fontFamily: "hind, sans-serif;",
},
palette: {
primary: {
main: "#000000",
},
secondary: {
main: "#ffffff",
},
},
})
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<MuiThemeProvider theme={theme}>
<Routes />
</MuiThemeProvider>
</React.StrictMode>
</Provider>,
document.getElementById("root")
)
In routes/index.tsx I have this
import * as React from "react"
import { Router } from "@reach/router"
import BaseTemplate from "@templates/basetemplate/BaseTemplate"
import ContentsPage from "@pages/content/ContentsPage"
const Routes = () => (
<BaseTemplate>
<Router>
<ContentsPage path="/" />
</Router>
</BaseTemplate>
)
export default Routes
And in my ContentsPage.tsx I have
import * as React from "react"
import { connect } from "react-redux"
import { RouteComponentProps } from "@reach/router"
import { IStoreState } from "~types/common"
import { ContentPageActions, IContentPageActions } from "./contentPageActions"
import * as s from "./ContentPage.scss"
import { ContentCardGeneric } from "@qmiui/contentui"
import { Content, ContentStory, ContentVideo } from "@models"
import i18n from "translations/i18n"
import { Dispatch } from "redux"
import { IFetchContents } from "@pages/content/contentPageActions"
// Redux connection
interface IStateToProps {
contents: Content[]
}
// declare actions
type TDispatchToProps = IContentPageActions
type TContentPageProps = TDispatchToProps & IStateToProps
interface IContentsPageState {
contentsCard: JSX.Element[]
}
class ContentsPage extends React.PureComponent<
RouteComponentProps & TContentPageProps,
IContentsPageState
> {
static getDerivedStateFromProps(nextProps: TContentPageProps, state: IContentsPageState) {
if (nextProps.contents) {
return { contentsCard: ContentsPage.renderContentCard(nextProps.contents) }
}
}
static renderContentCard = (contents: Content[]): JSX.Element[] => {
const contentsCard: JSX.Element[] = []
contents.forEach((content: Content, index: number) => {
switch (content.constructor) {
case ContentStory:
contentsCard.push(ContentsPage.renderGenericContentCard(content, index))
break
case ContentVideo:
contentsCard.push(ContentsPage.renderGenericContentCard(content, index))
break
default:
contentsCard.push(ContentsPage.renderGenericContentCard(content, index))
break
}
})
return contentsCard
}
static renderGenericContentCard = (content: Content, index: number): JSX.Element => {
return (
<div
className={s.container}
key={content.id}
data-qa="genericContentCardDiv"
id={`genericContentCard${index}`}
>
<ContentCardGeneric
title={content.name}
statusType={content.state}
statusText={i18n.t([`status.${content.state}`, content.state])}
type={content.type}
typePlaceholder={i18n.t(`contentType.${content.type}`)}
source={content.source}
systemOrigin={i18n.t(`originSystem.${content.originSystem}`)}
ingestionDate={Date.parse(content.versionInfo.timestamp)}
ingestionDateFormat={i18n.t("date.longFormat")}
ingestionDateLocale={i18n.t("date.shortLanguage")}
/>
</div>
)
}
state: IContentsPageState = {
contentsCard: [],
}
componentDidMount() {
this.props.fetchContents(true, false, 0, 100, true, [])
}
public render(): JSX.Element {
return <div>{this.state.contentsCard}</div>
}
}
const mapStateToProps = (state: IStoreState) => ({
contents: state.contentPage.contents,
})
const mapDispatchToProps = (dispatch: Dispatch<IFetchContents>) => ({
fetchContents(
execute: boolean,
isOriginId: boolean,
pageOffset: number,
pageSize: number,
sortAscending: boolean,
sortBy: string[]
) {
dispatch(
ContentPageActions.fetchContents(
execute,
isOriginId,
pageOffset,
pageSize,
sortAscending,
sortBy
)
)
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(ContentsPage)
However, I have this error
Error:(10, 8) TS2322: Type '{ path: string; contents: undefined[]; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<Pick<Partial<{}> & { path?: string; default?: boolean; location?: WindowLocation; navigate?: NavigateFn; uri?: string; } & IContentPageActions & IStateToProps, "path" | ... 5 more ... | "fetchContentsFail">, any, any>> & Readonly<...> & Readonly<...>'.
Property 'contents' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<Pick<Partial<{}> & { path?: string; default?: boolean; location?: WindowLocation; navigate?: NavigateFn; uri?: string; } & IContentPageActions & IStateToProps, "path" | ... 5 more ... | "fetchContentsFail">, any, any>> & Readonly<...> & Readonly<...>'.
Any idea how this could be fixed?
Just my 2c worth, but I think the correct approach is to mark the prop as optional, as in
type Props = RouteComponentProps<{ foo: string }>
Then handle that potentially undefined foo prop fully in your component. The TypeScript error about it being potentially undefined is desirable, useful and correct - in some cases it may indeed be undefined since there's no explicit guarantee you can make that component will always appear directly nested within a <Router> and you should write code to handle it if type safety is the objective.
Im using a Uniontype for my props like so

I think its not a valid practice to patch the solution with simply making the prop optional. In my project its a MobX store that is adding to the props, and to make that optional means all the injected instances need to be handled if undefined. In a weigh off, I would rather use another routing solution, than weaken my types. There must be some solution to this surely?
Closing per the discussion in #294
This is kind of a continuation of my first issue I created here. It is, again, more of an issue with "how do I use TypeScript correctly" but I think it would great to have some help regarding these issues in the Reach Router documentation because they are not very intuitive to solve with only moderate TypeScript experience.
Let's say I have a Page component called
TodoListPage:interface Props extends RouteComponentProps { todoListId: string; } class TodoListPage extends React.Component<Props, any> {}I'm expecting to get the
todoListIdprop from the router:<TodoListPage path="/todo-lists/:todoListId" />How do I make this clear to TypeScript? What is the recommended approach to get around this issue?
It's because reach-router doesn't have you set the dynamic parameter declaratively, but it it's own non-TS language (/some/path/:name), so TS can't possibly know about it.
The same issue with 'bob'.match(/(?<test>o)/).groups['test'], the language being RegExp, TS won't know groups can have element test.
BUT, we can patch this to make the reach API declarative:
import {RouteComponentProps} from '@reach/router';
import {Component, ComponentType}
from 'react';
function Route<
T extends RouteComponentProps,
U extends keyof T
>(Type :ComponentType<T>, dynamic :U, path :string = ':') {
return class extends Component<Omit<T, U>> {
render = () => (
(this.props as any)[dynamic] != null &&
<Type { ...(this.props as any) } />
);
static defaultProps = {
path : path.replace(/(?<=:)/, dynamic as string)
}
};
}
now you can do this;
const RoutableTodoListPage = Route(TodoListPage, 'todoListId', '/todo-lists/:');
...
<RoutableTodoListPage someOther="Props" />
You can wrap the class immediately if you're not going to use any static members/different paths for it, and I'd just always do it for function-components, eg:
const TodoListPage = Route((class extends React.Component<Props, any> {}), 'todoListId', '/todo-lists/:');
Most helpful comment
Try this: