so in react I have an App component that is rendering several child components like this:
App
render() {
return (
//JSX inside
<BrowserRouter>
<div>
<Header />
<Switch>
<Route exact path="/" component={Courses} />
<Route exact path="/courses/create" component={() => <CreateCourse email={this.state.emailAddress} pass={this.state.password} />} />
<Route exact path="/courses/:id/update" component={() => <UpdateCourse email={this.state.emailAddress} pass={this.state.password} />} />
<Route exact path="/courses/:id" component={() => <CourseDetail email={this.state.emailAddress} pass={this.state.password} />} />
<Route exact path="/signin" component={ () => <UserSignIn signIn={this.signIn}/>} /> {/*pass in the signIn() in a prop called signIn to the UserSignIn component*/}
<Route exact path="/signup" component={UserSignUp} />
{/* <Route exact path="/signout" component={UserSignOut} /> */}
</Switch>
</div>
</BrowserRouter>
);
}
In this component I have params so that I am able to see a course by its id:
CourseDetail
componentDidMount() {
const {match: { params }} = this.props; //I used a code snippet from this video https://scotch.io/courses/using-react-router-4/route-params
//fetch data from API
axios
.get(`http://localhost:5000/api/courses/${params.id}`)
.then(results => {
//results param came back as data from api
this.setState({
//set state by setting the courses array to hold the data that came from results
course: results.data,
user: results.data.user
});
//console.log(results); //By console logging I was able to see that I am getting each individual course's info in the data object
});
}
//this method will be for deleting a course
handleDelete() {
const { match: { params }, history } = this.props;
axios.delete(`http://localhost:5000/api/courses/${params.id}`, {
auth: {
username: this.props.email,
password: this.props.pass
}
}).then(() => {
history.push("/"); //I used the history object and have it push to the homepage, that way every time I delete a course I am redirected to (/) afterwards
});
}
the error I am getting when I try to navigate to the CourseDetail component that uses params is:
can someone help?
You need to pass props like this read here
component={props => <CourseDetail {...props} email={this.state.emailAddress} pass={this.state.password} />} />
The props passed to courseDetails component do not have any prop name match and in your componentDidMount you're doing this
const {match: { params }} = this.props;
Here match will be undefined so you can access params
You can understand by this example
let a = {a:{b:1}}
let {x:{b,}} = a
The above code is same as below
"use strict";
var a = {
a: {
b: 1
}
};
var b = a.x.b;
So eventually here if during destructuring if you don't have match as params you're trying to access
(this.props.match).params
|
|__________ This is undefined you end up `undefined.params`
Match is not defined because you didn't pass it down the component as props.
To do that
<Route exact path="/courses/:id/update" component={(routeProps) => <UpdateCourse email={this.state.emailAddress} pass={this.state.password} routeProps = {routeProps} />} />
you can then get your Match property via
routeProps.
const {match} = this.routeProps;
Or simply use the property spread notation which will spread out the properties in routeProps as discrete properties in your component.
Example,
<Route exact path="/courses/:id/update" component={(routeProps) => <UpdateCourse email={this.state.emailAddress} pass={this.state.password} {...routeProps} />} />
This is equivalent to writing-
<Route exact path="/courses/:id/update" component={(routeProps) => <UpdateCourse email={this.state.emailAddress} pass={this.state.password} Match = {this.routeProps.Match} Location = {this.routeProps.Location}/>} History = {this.routeProps.History />
When you write this
const {match: { params }} = this.props;
It means you are expecting props to have a match attribute like below:
params = this.props.match.params;
This is why you are getting the specified error.
If you want to assign a variable the props use this
const params = props;
Note: [If you surround a variable with bracket const {match} = props; it means you are expecting a key match in props.
Related
Somehow I came to a problem of thinking of how to make that if the person clicks on a link, it should be redirected to sign-in page if not authorized and to that page if authorized. This sounds easy, but the problem is that I want to make that if the user redirected to one page where it should be authorized, the user authorizes and redirect to the same page as he clicked.
For now, I have a protected route that looks like this: (I have fromPath argument for next redirection but that does not work for me.)
const ProtectedRoute = ({
isAllowed,
redirectPath = "/sign-in",
fromPath = null,
children,
}) => {
const dispatch = useDispatch();
if (fromPath) dispatch(setURLPath(fromPath));
if (!isAllowed) {
return <Navigate to={fromPath} replace />;
}
return children ? children : <Outlet />;
};
And here how it looks from the App.js side:
<Suspense fallback={<Spinner />}>
<GlobalStyle />
<Routes>
<Route
path='/'
element={
<ProtectedRoute
isAllowed={roleLevel > 0}
/>
}
>
<Route path='bookings' element={<BookingsPage />} />
<Route path='single-booking/:id' element={<SingleBookingPage />} />
<Route path='documents' element={<DocumentsPage />} />
<Route path='my-account' element={<MyAccountPage />} />
<Route path='reservation' element={<ReservationPage />} />
</Route>
</Route>
<Route path='*' element={<NotFoundPage />} />
</Routes>
</Suspense>
The ProtectedRoute component should grab the current location object for the route being accessed and pass this in route state to the login route.
import { useLocation } from 'react-router-dom';
const ProtectedRoute = ({
isAllowed,
redirectPath = "/sign-in",
fromPath = null,
children,
}) => {
const location = useLocation();
const dispatch = useDispatch();
if (fromPath) dispatch(setURLPath(fromPath));
if (!isAllowed) {
return <Navigate to={fromPath} replace state={{ from: location }} />;
}
return children ? children : <Outlet />;
};
The login component should then access the passed route state and redirect back to the original route being accessed.
const location = useLocation();
const navigate = useNavigate();
...
const login = () => {
...
const { from } = location.state || { from: { pathname: "/" } };
navigate(from, { replace: true });
};
You can achieve this by passing some params (next_route) for example. and keep it along the process in signin so that when he finishes he can ge reredirected to the right place (next_route)
I have a router:
<Switch>
<Route exact path="/">
<CustomPaddingContainer padding="0 0.5em 1.5em">
<TableViewComponent columns={tableAccessors} />
</CustomPaddingContainer>
</Route>
<Route path="/new-objective">
<AddNewObjectiveComponent onSubmit={onSubmitObjective} onCancel={onCancel} />
</Route>
<Route path="/new-kr">
<AddNewKrComponent onSubmit={onSubmitKR} onCancel={onCancel} />
</Route>
<Route path="/okr-details/:id">
<OkrDetailsWithParams />
</Route>
</Switch>
and I want to pass specific data from specific component to one of this Route when specific button will be clicked. to be more precise, I have this component:
const AddOKRButtons: FC<AddOKRButtonsProps> = ({ parentObjectiveId }) => {
const history = useHistory();
const onAddOkrButtonClick = () => {
history.push('/new-objective', { parentObjectiveId: parentObjectiveId });
};
const onAddKrButtonClick = () => {
history.push('/new-kr', { parentObjectiveId: parentObjectiveId });
};
return (
<OkrDetailsChildrenCardsButtonContainerCentered>
<ButtonGroup>
<LinkButton to="/new-objective" appearance="default" onClick={onAddOkrButtonClick}>
Add a new sub-objective
</LinkButton>
<LinkButton to="/new-kr" appearance="default" onClick={onAddKrButtonClick}>
Add a new key-result
</LinkButton>
</ButtonGroup>
</OkrDetailsChildrenCardsButtonContainerCentered>
);
};
Im trying to pass the **parentObjectiveId** which is coming from props to the /new-objective page or /new-kr page in order what button was clicked. After that Im trying to get that data in component where it should be with useLocation hook:
export const AddNewObjectiveComponent: FC<NonNullable<AddNewOKRProps>> = props => {
const location = useLocation();
console.log(location);
return(<div></div>)
}
and unfortunately i got undefined in the state key, where the data is probably should be:
Try to push history route like
history.push({
pathname: '/new-objective',
state: { parentObjectiveId: parentObjectiveId }
});
I hope it will be work for you. Thanks!
I'm trying to find a way to organize my routes to assist the dev who might be taking over my work in the future. I thought of separating my <Route /> entries into separate components and then just load those into a main component similar to how users are assigned groups.
The issue is that when using more than one component only the first one works. This might not be the most react way of doing this so I'm also open to alternatives.
Original route arrangement
const AllRoutes = () => {
return (
<Switch>
{/* public routes*/}
<Route path={'/about'} component={AboutView} />
<Route path={'/project'} component={ProjectView} />
<Route path={'/contact'} component={ContactView} />
{/* auth routes */}
<Route path={'/login'} component={LoginView} />
<Route path={'/logout'} component={LogoutView} />
<Route component={Error404View} />
</Switch>
)
}
Separating the public routes from the auth ones:
const PublicRouteGroup = () => {
return (
<>
<Route path={'/about'} component={AboutView} />
<Route path={'/project'} component={ProjectView} />
<Route path={'/contact'} component={ContactView} />
</>
)
}
const AuthRouteGroup = () => {
return (
<>
<Route path={'/login'} component={LoginView} />
<Route path={'/logout'} component={LogoutView} />
</>
)
}
This way I can use it as such:
const AllRoutes = () => {
return (
<Switch>
<PublicRouteGroup /> {/* This works */}
<AuthRouteGroup /> {/* This doesn't */}
{/* This 404 is not a route group */}
<Route component={Error404View} />
</Switch>
)
}
Flipping <PublicRouteGroup /> and <AuthRouteGroup /> only changes the order:
const AllRoutes = () => {
return (
<Switch>
<AuthRouteGroup /> {/* This works */}
<PublicRouteGroup /> {/* This doesn't */}
{/* This 404 is not a route group */}
<Route component={Error404View} />
</Switch>
)
}
Update #1
This is thanks to #skyboyer. By moving the <Switch> to the child components and removing it from the AllRoutes component each component started to show. It appears adding the <Switch> in AllRoutes is allowing only the first hit to show which is as <Switch> does. But now by removing it it shows the 404 at the end of each page as well.
Basically, it looks like this:
const AllRoutes = () => {
return (
<>
<Route component={AuthRouteGroup} /> {/* This works */}
<Route component={PublicRouteGroup} /> {/* This also works */}
{/* This 404 is not a route group */}
<Route component={Error404View} /> {/* Always shown at the bottom */}
{/* Even putting the 404 in its own RouteGroup yields the same issue */}
</>
)
}
It appears this current set up of treating components like OOP classes you can extend from is the wrong approach. I've instead made use of arrays since these can be acted upon by the spread operator. It still accomplishes the same goal of organizing routes across an infinite number of groups which was what I was after.
Create the array for each group
const public_route_group = [
{path: '/about', component: AboutView},
{path: '/project', component: ProjectView},
{path: '/contact', component: ContactView},
]
const auth_route_group = [
{path: '/login', component: LoginView},
{path: '/logout', component: LogoutView},
]
const error_route_group = [
{component: Error404View} // No path required
]
const user_routes = [
...public_route_group,
...auth_route_group,
...error_route_group
]
Create the routes
const AllRoutes = () => {
return (
<Switch>
{user_routes.map((route, idx) => {
return <Route key={idx} {...route} />
})}
</Switch>
)
}
I figure this can also be modified further if you're using nested objects in your array.
I'd like to thank #skyboyer for providing an insight into this problem.
How about having it without Swtich at top-level
<Route component={PublicRouteGroup} />
<Route component={AuthRouteGroup} />
so they are rendered unconditionally. And then having extra Switch in your components like
const AuthRouteGroup = () => {
return (
<Switch>
<Route path={'/login'} component={LoginView} />
<Route path={'/logout'} component={LogoutView} />
<Switch/>
)
}
But why id did not work?
The reason is how Switch works:
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
See, even if AuthRouteGroup is not a Route, Switch anyway looks to its props.path. And once undefined for props.path matches any path and Switch renders only first matching Route you are getting only first component rendered.
[UPD] "does-not-match-any-route" View will work only at top level of Switch. Also there are no way to know if some nested children of sibling element has matched current route or not. So only way I see is listing all routes in single place.
Alternative that looks rather poor is having special route "/error404" and redirect user to it from inside of other components(but who should decide? and where? and when?).
I'm trying to change the state title value to the value I give in the router, but I do not know why it does not work. This code is compiled, but the title is an empty string all the time.
class Header extends React.Component {
state = {
title: '',
};
updateTitle(title) {
this.setState({ title });
}
render() {
const { title } = this.state;
return (
<Typography>
{title}
</Typography>
<Switch>
<Route
exact
path="/"
render={() => (<DashboardPage updateTitle={this.updateTitle} />)}
title="Dashboard"
/>
<Route
path="/payment"
render={() => (<PaymentPage updateTitle={this.updateTitle} />)}
title="Payment"
/>
</Switch>
)};
You have to bind the Component method to pass .this context. So updateTitle = (title)=> {}
You should wrap this.updateTitle in an arrow function like below. Then you can call updateTitle from DashBoardPage.
<Route
path="/dashboard"
render={() => (<DashBoardPage updateTitle={(title) => this.updateTitle(title)} />)}
title="Dashboard"
/>
For example, your DashBoardPage component could look something like below. By calling updateTitle which we have passed as a prop, we are changing the title in the parent component.
DashBoardPage.js
const DashBoardPage = ({ updateTitle }) => {
updateTitle('paymentPage');
return <div>This is the dashboard page</div>;
};
However, i would strongly advise against doing this. A better approach would be to have a Page component. Then DashBoardPage and PaymentPage return a Page component and pass the title as prop.
I am trying to build a form-wizard. I set up the wizard via react-router and used formik for the forms. Now I ran into a problem while creating a customizable radio-button. I used the react-custom-radio library for that.
When I use the radio-buttons outside of the routes, it is working as it should (code at the bottom of the post).
When I use the with the router, the props are passed down to the child component:
<Route path="/form/location" render={(props) => (<Pricing {...props} />)} />
Inside the child component, I access the props the same way, as I did it in the parent, where it worked.
const Pricing = (props) => {
const {
values,
touched,
errors,
setFieldValue,
setFieldTouched,
} = props;
return (
<div>
<MyRadio
value={values.car}
onChange={setFieldValue}
onBlur={setFieldTouched}
error={errors.car}
touched={touched.car}
/>
</div>
);
}
But now I always get the error Cannot read property 'car' of undefined.
Why doesn't it work if there's a router in between?
If I do it like that, it works:
<Form>
<Switch>
<Redirect from="/" exact to="/form/location" />
<Route path="/form/location" render={(props) => (<Pricing {...props} />)} />
</Switch>
<MyRadio
value={values.car}
onChange={setFieldValue}
onBlur={setFieldTouched}
error={errors.car}
touched={touched.car}
/>
</Form>
The props given to the render function are the route props listed in the documentation. What you want to do in this case is to pass down the props from the parent component, not the route props:
class ParentComponent extends React.Component {
render() {
const { props } = this;
const {
values,
touched,
errors,
setFieldValue,
setFieldTouched,
} = props;
return (
<Form>
<Switch>
<Redirect from="/" exact to="/form/location" />
<Route
path="/form/location"
render={() => <Pricing {...props} />}
/>
</Switch>
<MyRadio
value={values.car}
onChange={setFieldValue}
onBlur={setFieldTouched}
error={errors.car}
touched={touched.car}
/>
</Form>
);
}
}
And if you need both Formik's props and this component's you could do:
render={(formikProps) => <Pricing {...formikProps}, {...props} />}
That will create a long list of attributes from both props for Pricing to use.