Get movieId from router v6 to component (movieDB) [duplicate] - reactjs

How can i access url parameter in my react component ?
App.js
<Route path="/question/:id" element={<QuestionView />} />
QuestionView.js
class QuestionView extends React.Component {
render() {
const { questions, users } = this.props;
const {id} = ???

Issue
In react-router-dom v6 the Route components no longer have route props (history, location, and match), and the current solution is to use the React hooks "versions" of these to use within the components being rendered. React hooks can't be used in class components though.
To access the match params with a class component you must either convert to a function component, or roll your own custom withRouter Higher Order Component to inject the "route props" like the withRouter HOC from react-router-dom v5.x did.
Solution
I won't cover converting a class component to function component. Here's an example custom withRouter HOC:
const withRouter = WrappedComponent => props => {
const params = useParams();
// etc... other react-router-dom v6 hooks
return (
<WrappedComponent
{...props}
params={params}
// etc...
/>
);
};
And decorate the component with the new HOC.
export default withRouter(Post);
This will inject a params prop for the class component.
this.props.params.id

HOC withRouter TypeScript version with generic Params
withRouter.tsx
import { ComponentType } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
export interface WithRouterProps<T = ReturnType<typeof useParams>> {
history: {
back: () => void;
goBack: () => void;
location: ReturnType<typeof useLocation>;
push: (url: string, state?: any) => void;
}
location: ReturnType<typeof useLocation>;
match: {
params: T;
};
navigate: ReturnType<typeof useNavigate>;
}
export const withRouter = <P extends object>(Component: ComponentType<P>) => {
return (props: Omit<P, keyof WithRouterProps>) => {
const location = useLocation();
const match = { params: useParams() };
const navigate = useNavigate();
const history = {
back: () => navigate(-1),
goBack: () => navigate(-1),
location,
push: (url: string, state?: any) => navigate(url, { state }),
replace: (url: string, state?: any) => navigate(url, {
replace: true,
state
})
};
return (
<Component
history={history}
location={location}
match={match}
navigate={navigate}
{...props as P}
/>
);
};
};
MyClass.tsx
import { Component } from 'react';
import { withRouter, WithRouterProps } from './withRouter';
interface Params {
id: string;
}
type Props = WithRouterProps<Params>;
class MyClass extends Component<Props> {
render() {
const { match } = this.props;
console.log(match.params.id); // with autocomplete
return <div>MyClass</div>;
}
}
export default withRouter(MyClass);

Here's the code example I'm using in my project to get the id from the URL:
import React from 'react'
import {Button} from 'antd'
import {useParams} from 'react-router-dom'
const DeleteUser = () => {
const {id} = useParams()
const handleDelete = async () => {
// handle delete function
}
return (
<Button onClick={handleDelete}>Delete User</Button>
)
}
export default DeleteUser

If you would like to use a class, then you will need to wrap it with the withRouter. I provide an example below:
This is my class for the movie form:
class MovieForm extends Form {
state = {
data: {
title: "",
genreId: "",
numberInStock: "",
dailyRentalRate: ""
},
genres: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string()
.required()
.label("Title"),
genreId: Joi.string()
.required()
.label("Genre"),
numberInStock: Joi.number()
.required()
.min(0)
.max(100)
.label("Number in Stock"),
dailyRentalRate: Joi.number()
.required()
.min(0)
.max(10)
.label("Daily Rental Rate")
};
componentDidMount() {
const genres = getGenres();
this.setState({ genres });
// const movieId = this.props.match.params.id;
const movieId = this.props.params.id;
if (movieId === "new") return;
const movie = getMovie(movieId);
if (!movie) return this.props.history.replace("/not-found");
this.setState({ data: this.mapToViewModel(movie) });
}
mapToViewModel(movie) {
return {
_id: movie._id,
title: movie.title,
genreId: movie.genre._id,
numberInStock: movie.numberInStock,
dailyRentalRate: movie.dailyRentalRate
};
}
doSubmit = () => {
saveMovie(this.state.data);
this.props.navigate("/movies");
};
render() {
return (
<div>
<h1>Movie Form</h1>
<form onSubmit={this.handleSubmit}>
{this.renderInput("title", "Title")}
{this.renderSelect("genreId", "Genre", this.state.genres)}
{this.renderInput("numberInStock", "Number in Stock", "number")}
{this.renderInput("dailyRentalRate", "Rate")}
{this.renderButton("Save")}
</form>
</div>
);
}
}
I write a wrapper outside of the class:
const withRouter = WrappedComponent => props => {
const params = useParams();
const navigate = useNavigate();
return (
<WrappedComponent
{...props}
params={params}
navigate={navigate}
/>
);
};
Now, at the end of the file I will export it like below:
export default withRouter(MovieForm);
Insdie the withRouter, I get all the functions that I will use later inside the class:
const params = useParams();
const navigate = useNavigate();

TypeScript version
withRouter.tsx
import React from 'react';
import {
useLocation,
useNavigate,
useParams,
NavigateFunction,
Params,
Location,
} from 'react-router-dom';
export interface RouterProps {
router: {
navigate: NavigateFunction;
readonly params: Params<string>;
location: Location;
}
}
function withRouter(Component: React.ComponentType<RouterProps>) {
const ComponentWithRouterProp: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
return (
<Component
router={{ location, navigate, params }}
/>
);
};
return ComponentWithRouterProp;
}
export default withRouter;
MyComponent.tsx
import React from 'react';
import { connect } from 'react-redux';
import { RootState } from '##/redux/store';
import {
addSettings,
updateSettings,
} from '##/redux/mySlice';
import withRouter, { RouterProps } from '##/withRouter';
const mapState = (state: RootState) => ({
myStore: state.variation.myStore,
});
const mapDispatch = {
addSettings,
updateSettings,
};
type IProps = ReturnType<typeof mapState> & typeof mapDispatch & RouterProps;
class MyComponent extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
}
onNavigateHome = () => {
this.props.router.navigate('/');
}
render(): React.ReactNode {
return (
<div className="test" onClick={this.onNavigateHome}>test</div>
);
}
}
export default withRouter(connect(mapState, mapDispatch)(MyComponent));

I had a similar issue, described here:
Params from React Router with class components and typescript
I created a new functional component, so I can use useParams():
import React from 'react';
import { useParams } from 'react-router';
type Props = {
children: JSX.Element
};
export const WithParams: React.FC<Props> = (props) => {
const params = useParams();
return React.cloneElement(props.children, {...props.children.props, ...params });
};
and added it to my Route.element
<Route path="/Contacts/VerifyEmailAddress/:id"
element={
<WithParams>
<VerifyEmail />
</WithParams>
}>
</Route>
and added the parameters I need to the props of my child component.
export class VerifyEmailProps {
public id?: string;
}

Related

React Router 6 and TypeScript - rewrite WithRouter to be a typed wrapper [duplicate]

How can i access url parameter in my react component ?
App.js
<Route path="/question/:id" element={<QuestionView />} />
QuestionView.js
class QuestionView extends React.Component {
render() {
const { questions, users } = this.props;
const {id} = ???
Issue
In react-router-dom v6 the Route components no longer have route props (history, location, and match), and the current solution is to use the React hooks "versions" of these to use within the components being rendered. React hooks can't be used in class components though.
To access the match params with a class component you must either convert to a function component, or roll your own custom withRouter Higher Order Component to inject the "route props" like the withRouter HOC from react-router-dom v5.x did.
Solution
I won't cover converting a class component to function component. Here's an example custom withRouter HOC:
const withRouter = WrappedComponent => props => {
const params = useParams();
// etc... other react-router-dom v6 hooks
return (
<WrappedComponent
{...props}
params={params}
// etc...
/>
);
};
And decorate the component with the new HOC.
export default withRouter(Post);
This will inject a params prop for the class component.
this.props.params.id
HOC withRouter TypeScript version with generic Params
withRouter.tsx
import { ComponentType } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
export interface WithRouterProps<T = ReturnType<typeof useParams>> {
history: {
back: () => void;
goBack: () => void;
location: ReturnType<typeof useLocation>;
push: (url: string, state?: any) => void;
}
location: ReturnType<typeof useLocation>;
match: {
params: T;
};
navigate: ReturnType<typeof useNavigate>;
}
export const withRouter = <P extends object>(Component: ComponentType<P>) => {
return (props: Omit<P, keyof WithRouterProps>) => {
const location = useLocation();
const match = { params: useParams() };
const navigate = useNavigate();
const history = {
back: () => navigate(-1),
goBack: () => navigate(-1),
location,
push: (url: string, state?: any) => navigate(url, { state }),
replace: (url: string, state?: any) => navigate(url, {
replace: true,
state
})
};
return (
<Component
history={history}
location={location}
match={match}
navigate={navigate}
{...props as P}
/>
);
};
};
MyClass.tsx
import { Component } from 'react';
import { withRouter, WithRouterProps } from './withRouter';
interface Params {
id: string;
}
type Props = WithRouterProps<Params>;
class MyClass extends Component<Props> {
render() {
const { match } = this.props;
console.log(match.params.id); // with autocomplete
return <div>MyClass</div>;
}
}
export default withRouter(MyClass);
Here's the code example I'm using in my project to get the id from the URL:
import React from 'react'
import {Button} from 'antd'
import {useParams} from 'react-router-dom'
const DeleteUser = () => {
const {id} = useParams()
const handleDelete = async () => {
// handle delete function
}
return (
<Button onClick={handleDelete}>Delete User</Button>
)
}
export default DeleteUser
If you would like to use a class, then you will need to wrap it with the withRouter. I provide an example below:
This is my class for the movie form:
class MovieForm extends Form {
state = {
data: {
title: "",
genreId: "",
numberInStock: "",
dailyRentalRate: ""
},
genres: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string()
.required()
.label("Title"),
genreId: Joi.string()
.required()
.label("Genre"),
numberInStock: Joi.number()
.required()
.min(0)
.max(100)
.label("Number in Stock"),
dailyRentalRate: Joi.number()
.required()
.min(0)
.max(10)
.label("Daily Rental Rate")
};
componentDidMount() {
const genres = getGenres();
this.setState({ genres });
// const movieId = this.props.match.params.id;
const movieId = this.props.params.id;
if (movieId === "new") return;
const movie = getMovie(movieId);
if (!movie) return this.props.history.replace("/not-found");
this.setState({ data: this.mapToViewModel(movie) });
}
mapToViewModel(movie) {
return {
_id: movie._id,
title: movie.title,
genreId: movie.genre._id,
numberInStock: movie.numberInStock,
dailyRentalRate: movie.dailyRentalRate
};
}
doSubmit = () => {
saveMovie(this.state.data);
this.props.navigate("/movies");
};
render() {
return (
<div>
<h1>Movie Form</h1>
<form onSubmit={this.handleSubmit}>
{this.renderInput("title", "Title")}
{this.renderSelect("genreId", "Genre", this.state.genres)}
{this.renderInput("numberInStock", "Number in Stock", "number")}
{this.renderInput("dailyRentalRate", "Rate")}
{this.renderButton("Save")}
</form>
</div>
);
}
}
I write a wrapper outside of the class:
const withRouter = WrappedComponent => props => {
const params = useParams();
const navigate = useNavigate();
return (
<WrappedComponent
{...props}
params={params}
navigate={navigate}
/>
);
};
Now, at the end of the file I will export it like below:
export default withRouter(MovieForm);
Insdie the withRouter, I get all the functions that I will use later inside the class:
const params = useParams();
const navigate = useNavigate();
TypeScript version
withRouter.tsx
import React from 'react';
import {
useLocation,
useNavigate,
useParams,
NavigateFunction,
Params,
Location,
} from 'react-router-dom';
export interface RouterProps {
router: {
navigate: NavigateFunction;
readonly params: Params<string>;
location: Location;
}
}
function withRouter(Component: React.ComponentType<RouterProps>) {
const ComponentWithRouterProp: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
return (
<Component
router={{ location, navigate, params }}
/>
);
};
return ComponentWithRouterProp;
}
export default withRouter;
MyComponent.tsx
import React from 'react';
import { connect } from 'react-redux';
import { RootState } from '##/redux/store';
import {
addSettings,
updateSettings,
} from '##/redux/mySlice';
import withRouter, { RouterProps } from '##/withRouter';
const mapState = (state: RootState) => ({
myStore: state.variation.myStore,
});
const mapDispatch = {
addSettings,
updateSettings,
};
type IProps = ReturnType<typeof mapState> & typeof mapDispatch & RouterProps;
class MyComponent extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
}
onNavigateHome = () => {
this.props.router.navigate('/');
}
render(): React.ReactNode {
return (
<div className="test" onClick={this.onNavigateHome}>test</div>
);
}
}
export default withRouter(connect(mapState, mapDispatch)(MyComponent));
I had a similar issue, described here:
Params from React Router with class components and typescript
I created a new functional component, so I can use useParams():
import React from 'react';
import { useParams } from 'react-router';
type Props = {
children: JSX.Element
};
export const WithParams: React.FC<Props> = (props) => {
const params = useParams();
return React.cloneElement(props.children, {...props.children.props, ...params });
};
and added it to my Route.element
<Route path="/Contacts/VerifyEmailAddress/:id"
element={
<WithParams>
<VerifyEmail />
</WithParams>
}>
</Route>
and added the parameters I need to the props of my child component.
export class VerifyEmailProps {
public id?: string;
}

How would one pass a component to a helper?

I want to pass a component to a helper and have that helper return an array of objects, each with a component node...
// helpers.ts
import { LINKS } from '../constants';
// error on the next line: Cannot find name 'Component'. ts(2304)
const createLinks = (component: Component) => {
return LINKS.map((props) => {
return ({
content: <Component {...props} />,
id: props.id
});
});
};
// component.tsx
import { List, SpecialLink } from '../components';
import { createLinks } from '../helpers';
const LinkList = () => {
const links = createLinks(SpecialLink);
return <List items={links}>
}
You should use the ComponentType type of react, so the component argument can be class component or function component.
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
import React from 'react';
import { ComponentType } from 'react';
const LINKS: any[] = [];
const createLinks = (Component: ComponentType) => {
return LINKS.map((props) => {
return {
content: <Component {...props} />,
id: props.id,
};
});
};

What is the shortest way to add redux to react-navigation?

I'm integrating redux to my react-native application. Want to introduce Provider and to wrap screens in it. I'm using such constructions to register new screen:
Navigation.registerComponent('Login', () => LoginScreen);
As I see in documentation I need to wrap every component in <Provider store={store}>
And I'm just wondering if exist another way to do this, because if I have 10 screens(for example) in my app, I need to wrap them in provider ten times. I'll get code duplicates, but htis is not good
You can achieve this by creating an AppContainer component with all the components and wrap the AppContainer component inside the Provider.
import React, { Component } from "react";
import { createAppContainer } from "react-navigation";
import { createStackNavigator } from "react-navigation-stack";
import { Provider as StoreProvider } from "react-redux";
const MainNavigator = createStackNavigator({
firstcomponentpath: FirstComponent,
secondcomponentpath: SecondComponent
)}
const AppContainer = createAppContainer(MainNavigator);
export default class App extends Component {
super(props);
this.state = {};
}
render() {
return (
<Provider store={store}>
<AppConatiner />
</Provider>
)}
You can try to directly add navigation to your store at the init :
HomePage.tsx
interface HomePageProps {
navigation: {
navigate: (pathName: string) => void;
};
}
const HomePage: FunctionComponent<HomePageProps> = (props) => {
const { navigation } = props;
initStore(navigation);
Store.ts
export const fetchInitStore = (props: HomePageProps) => {
const { navigation } = props;
const action: Actions = {
type: ActionType.INIT_STORE_STATE,
payload: {
Navigation: navigation,
},
};
return action;
};
export const initStore = (props: HomePageProps) => {
(store.dispatch as ThunkDispatch<State, unknown, Actions>)(fetchInitStore(props));
};
In you reducer :
case ActionType.INIT_STORE_STATE:
return actions.payload;
Then you can use it in other cases :
state.Navigation.navigation.navigate('Login');

react, redux with typescript error: Type '{}' is missing the following properties from type 'IProps': email, token

I am using HOC with redux, and I have been running into the error: Type '{}' is missing the following properties from type 'IProps': email, token. The HOC is supposed to inject the props(email and token) and state to the Lower component. But It is not passing them in this case. And the wrapper function (withLogin) does not appear in the react tree components in dev tools.
My HOC containing function (withLogin.tsx) looks like:
import * as React from "react";
import { connect, MapStateToProps } from "react-redux";
import { Dispatch } from 'redux';
import { propagateError } from "./actions";
import { Diff } from 'utility-types';
export interface IWithLoginProps {
email: string;
token: string;
}
type HocProps =
IDispatchProps & {
// here you can extend ConnectedHoc with new props
users: string
};
interface IDispatchProps {
cleanError(): void;
}
export default function WithLogin<T extends IWithLoginProps>(
BaseComponenet
): React.ComponentType<HocProps> {
class HOC extends React.Component<
HocProps,
{
hash: string;
}
> {
constructor(props) {
super(props);
this.state = {
hash: window.top.location.hash
};
}
render() {
return <BaseComponenet {...this.state} {...this.props} />;
}
}
const mapDispatchToProps = (dispatch: Dispatch): IDispatchProps => {
return {
cleanError: () => dispatch(propagateError(null))
};
};
// #ts-ignore
return connect<{}, IDispatchProps, Diff<HocProps, IWithHistoryProps>, {}>(
null,
mapDispatchToProps
)(HOC);
}
And my BaseComponent(App.tsx) looks like:
import React from "react";
import { connect } from "react-redux";
import { withLoginProps } from "./withLogin";
interface IStateProps {
usernames: string[];
}
interface IProps extends IWithLoginProps {}
const App: React.StatelessComponent <IProps & IStateProps> = (props) => {
return (
<div>
{props.users}
</div>
);
}
const mapStateToProps = (state: IRootState): IStateProps => {
return {
usernames: state.users
};
};
export default connect<IStateProps, null, null, IRootState>(
mapStateToProps, null)(withLogin(App));
My index.tsx:
import * as React from 'react';
import App from './App';
const Root: React.StatelessComponent<{}> = () => (
<div>
<Provider store={store}>
<App />
</Provider>
</div>
);
render(<Root />, document.getElementById(renderRoot));
};
Looks like an issue with how you are importing the default import, that is try changing from:
import { withLoginProps } from "./withLogin";
to:
import withLoginProps from "./withLogin";

Unable to call props functions in componentDidMount

From Party.Container where is connected Party with mapStateToProps and mapDispatchToProps, are sent two functions to Party (fetchData and fetchFooter)
They worked until I implemented in project eslint:"airbnb", and now it's constantly getting this error "Must use destructuring props assignment react/destructuring-assignment".
const mapActionsToProps = {
fetchData,
fetchDataFooter,};
--- these are functions
componentDidMount() {
this.props.fetchData();
this.props.fetchDataFooter(); }
This is the component
import { connect } from 'react-redux';
import { fetchData, fetchDataFooter } from './actions';
import Party from './Party';
const mapStateToProps = state => ({
wishlist: state.wishlist,
cart: state.cart,
});
const mapActionsToProps = {
fetchData,
fetchDataFooter,
};
export default connect(mapStateToProps, mapActionsToProps)(Party);
This is COntainer
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Header from '../../components/Header/Header';
import Content from './Content/Content.Container';
import styles from './Party.module.scss';
import Footer from '../../components/Footer/Footer';
const propTypes = {
wishlist: PropTypes.shape.isRequired,
cart: PropTypes.shape.isRequired,
// fetchData: PropTypes.func.isRequired,
// fetchDataFooter: PropTypes.func.isRequired,
};
class Party extends Component {
componentDidMount() {
// this.props.fetchData();
// this.props.fetchDataFooter();
}
render() {
const { wishlist, cart } = this.props;
let name;
let profilePicture;
let personWishlist;
let purchases;
let id;
if (wishlist.isFulfilled === true) {
const listId = wishlist.payloadData.data.filter(x => x.id === 1);
({ name } = listId[0].name);
({ profilePicture } = listId[0].picture);
({ personWishlist } = listId[0].wishList);
({ purchases } = listId[0].purchases);
({ id } = listId[0].id);
}
console.log(wishlist, cart);
return (
<div className={styles.Party}>
<Header />
<Content
name={name}
id={id}
profilePicture={profilePicture}
personWishlist={personWishlist}
purchases={purchases}
/>
<Footer
cart={cart}
/>
</div>
);
}
}
Party.propTypes = propTypes;
export default Party;
Can you try the one in below in your componentDidMount method as the error suggests:
componentDidMount() {
const { fetchData, fetchDataFooter } = this.props;
fetchData();
fetchDataFooter();
}
Actually, it means that your expressions should be destructured before usage.
E.g.: you're using:
...
this.props.fetchData();
this.props.fetchDataFooter();
...
You have to change it to:
const { fetchData, fetchDataFooter } = this.props;
fetchData();
fetchDataFooter();
Another solution is to disable this if you want to in your rules file.
"react/destructuring-assignment": [<enabled>, 'always'] - can be always or never.
See here for more information.

Resources