How to pass props to nested routes in React Router 4? - javascript

I have a component tree like this:
-App
--UserList
---UserItem
---UserItem
---UserItem
--User
I'm not being able to pass user data from UserItem to User. This is what I have:
App.js
export default class App extends Component {
state = { users: [] }
componentDidMount() {// fetch and setState}
render() {
return (
<BrowserRouter>
<Route
exact
path="/"
render={() => <UserList users={this.state.users} />}
/>
</BrowserRouter>
)
}
}
UserList.js
export default function({ users }) {
return (
<div>
{users.map(user => (
<UserItem user={user} key={`${user.id}`} />
))}
</div>
)
}
This is where the problem is: I want to pass the data from the parent component to the child User component, instead of having to fetch the user data from the API again.
UserItem.js
export default function({ user }) {
return (
<div>
<Link to="/user">{user.name}</Link>
<Route path={`/user/${user.name}`} render={() => <User user={user} />} />
</div>
)
}

I'm not sure what you're trying to implement here. Your app renders the UserList when then route is /. The UserList renders a UserItem component for each user in the array. Each UserItem simply renders a route specific to every user, which will render the User component if that route is triggered.
But if I'm not mistaken, the UserList will not be rendered if the route is anything but /, so if someone accesses user/..., the inner routes won't actually exist.
Essentially, this app will not render anything.
If you remove the exact keyword from the route in App, I think you'll get the result you are looking for. In this case, opening /user/<USER_NAME> will render the User element for that user.
Your question is regarding passing props into a component through a route, and the mechanism you've used is correct.
<Route path={...} render={() => <User user={user} />} />
This is actually right. See the code linked below. On changing the route to /user/User1, you'll see the name of "User1" rendered in the app.
See the working code here: https://codesandbox.io/s/18w3393767

You should use this.props.users in the UserItem component

i'm not sure but could you pass props like below, here i pass props to render and then to User Component
<Route path={`/user/${user.name}`} render={(props) => <User user={user} {...props} />} />

export default function({ users }) {
return (
<div>
{ this.props.users.map(user => (
//mistake here this.props.users.map not users.map
<UserItem user={user} key={`${user.id}`} />
))}
</div>
)
}

Related

ReactJS & Typescript: Creating a private route using router dom

I have seen many questions about this but none seem to work for me because they are old. The packages have been updated and the solutions for this have been stripped out. I do not want to use functional components to solve this.
I'm trying to create a private route with ReactJS and Typescript:
"react": "^17.0.2",
"react-router-dom": "^6.2.1",
"typescript": "^4.5.5",
But when I try to extend a Route:
export class PrivateRoute extends Route<PrivateRouteProps> {
public render(): JSX.Element {
return (
// ...
);
}
}
I get an error saying that I can't extend Route:
Type '(_props: PathRouteProps | LayoutRouteProps | IndexRouteProps) =>
ReactElement<any, string | JSXElementConstructor> | null' is not
a constructor function type.ts(2507)
And when I try to wrap a component around Route:
export class PrivateRoute extends Component<PrivateRouteProps> {
public render(): JSX.Element {
return (
// ...
<Route />
// ...
);
}
}
It let's me build and even run but I get a runtime error:
Uncaught Error: A <Route> is only ever to be used as the child of <Routes> element, never rendered directly. Please wrap your <Route> in a <Routes>.
Is there a correct way to do this? If not, is there a workaround for this without using functional components?
EDIT #1:
<Provider store={authStore}>
<BrowserRouter>
<Routes>
<Route path='/' element={<MainPage />} />
<Route path='/login' element={<LoginPage />} />
<PrivateRoute path='/dashboard' element={<DashboardPage />} />
<Route path='*' element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
</Provider>
EDIT #2:
For future reference: I have solved this using #SlothOverlord's answer but translated from functional component. Also, I'm using redux to fill the props.
private.outlet.tsx:
export class PrivateOutlet extends Component<AuthenticationProps> {
public render(): JSX.Element {
const user = this.props.user;
if (user) { // Check if logged in
return (
<>
{this.props.children}
<Outlet />
</>
);
}
return <Navigate to="/login" replace />; // Go back to login if not logged in
}
}
public.outlet.tsx:
export class PublicOutlet extends Component<AuthenticationProps> {
public render(): JSX.Element {
const user = this.props.user;
if (!user) { // Check if logged in
return (
<>
{this.props.children}
<Outlet />
</>
);
}
return <Navigate to="/" replace />; // Go to protected route if logged in
}
}
The routes I kept the same as in the answer.
EDIT #3:
Regarding the [duplicate] status: The question linked does not solve the problem for the versions stated at the beginning of the post. As #SlothOverlord stated at the beginning of his answer:
Routes work different in v6.
Not only that but Redirect does not exist in version 6. Many things are different. The decision to mark it duplicate was based on nothing except keyword matching of a flagging trigger happy mod.
This site is becoming a haven for self-important, misanthropic bullies that find in moderation a way to punish people for their own frustrations.
Routes work different in v6.
Note: Functional components example only:
You don't need to pass props to your routes anymore. There are two ways. Render route as outlet or a child. Outlet is basically your nested route, while children are direct children in your <Private/Public Outlet> component
Auth status is checked inside the route, not outside of it as in v5.
There are two examples below. Private outlet renders nested routes if logged in, public route renders children if not logged in.
For typescript, only FC type is needed to pass children, nothing else.
const PrivateOutlet: FC = ({ children }) => {
const { user } = useContext(ContextProvider); //Auth context
return user?.id ? ( //Check if logged in
<>
{children} //This is your children
<Outlet /> //This is your nested route
</>
) : (
<Navigate to="/login" replace /> //Go back to login if not logged in
);
};
const PublicOutlet: FC = ({ children }) => {
const { user } = useContext(ContextProvider); //Auth context
return !user?.id ? ( //Check if logged in
<>
{children} //This is your children
<Outlet /> //This is your nested route
</>
) : (
<Navigate to="/" replace /> //Go to protected route if logged in
);
};
export const MainRouter = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<PrivateOutlet />}> //Check auth for all nested routes
<Route index element={<MainPage />} /> //Outlet
<Route path="private" element={<MainPage2 />} /> //Outlet
</Route>
<Route
path="login" //Not nested in "/" Check for auth individually
element={
<PublicOutlet>
<LoginPage /> //Children
</PublicOutlet>
}
/>
//alternative for login
<Route path="login" element={ <PublicOutlet/> }>
<Route index element={<Login />} /> //Render as outlet.
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
);
};
You can also render nested routes from your other components. Just put "<Outlet/>" inside of it and it will render nested routes at that position. That way you can only place one dashboard component at the root of your route.

How to put <Route children={({match})} => ... > in Jsx fragment. React router dom v6

return (
<>
Some jsx..
<Route
path={modal.info ? `/fullInfo/${this.state.modal.id}`:`/preview/${this.state.modal.id}`}
element={({match}) => {
return (
<ModalWindow
modalVisible={Boolean(match)}
onCloseWindow={this.onCloseWindow}
modalContent={modal}
/>
)
}}
/>
</>
)
If I do that I get an error like: Route tag must be wrapped by Routes tag. I did this feature in a old version of react-router-dom but when I try to do it in the new one there is err..
In your App.js file, wrap the entire app with <BrowserRouter>
const AppWithRouter = () => <BrowserRouter><App /></BrowserRouter>
export default AppWithRouter
And then wrap all your routes in the new <Routes> tag (replaces switch):
<Routes>
<Route path="..." element={...} />
</Routes>
The Routes component effectively replaced the Switch component from v5, and it's required to wrap any Route components. Additionally, the Route components no longer take component, and render and children prop functions, the routed components must use the element prop that takes a ReactElement, a.k.a. JSX.
Wrap the Route component in a Routes component and since all routes are always exactly matched, just render the ModalWindow with the modalVisible prop set to true.
return (
<>
...Some jsx...
<Routes>
<Route
path={modal.info
? `/fullInfo/${this.state.modal.id}`
:`/preview/${this.state.modal.id}`
}
element={(
<ModalWindow
modalVisible
onCloseWindow={this.onCloseWindow}
modalContent={modal}
/>
)}
/>
</Routes>
</>
)

how to pass data between routes in react

I have a signup page signup.js after successful sign in I need to take those data to another route view sign in details page if I render sign in details then I am viewing it in the same page how to take it to another route ??If I do through window.name after refresh I couldn't view the data[my page layout]
1st page
class Validation extends Component {
constructor(props) {
super(props);
this.validate = this.validate.bind(this);
}
validate(detail,number) {
//i need to take this number to view page
});
}
render() {
return (
<div>
<SignupForm onAddDetail={this.validate} />
</div>);
}
}
export default Validation;
2nd page
class DetailsComponent extends Component {
render() {
let DetailNodes = this.props.details.map(detail =>
(
<Register key={detail.id}
emailId={detail.emailId} password={detail.password}
firstName={detail.firstName} lastName={detail.lastName}
location={detail.location} mobileNumber={detail.mobileNumber}>
</Register>
)
);
return (
<div> <br></br><br></br><br></br><br></br>
{DetailNodes[number-1]}//I need that number from 1st page and carried here
<br/>
</div>);
}
}
route.js
my route page
<Route path="/signup" component={Validation}/>
<Route path="/view" component={DetailsComponent} />
As option you can do it like this:
add field to state
success: true | false
and if signup is successfull
setState({success: true})
and in signup component add ternary
import {Redirect} from 'react-router-dom';
!success ? <SignUp /> : <Redirect to={{
pathname: '/view',
state: {data: signUpCredentials}
}}
/>
and in View component you can access it in
{props.location.state}
You can use the useHistory() hook of React router.
For eg:
const history = useHistory();
...
history.push('/');
You can pass props from Route render. You can do this in multiple ways. If your data is available at router level. Like below. Or setup up a Store for shared data
const myProps = {x,y,z}
<Route path="/signup" render={()=> <Validation myNewProp='someprop' />} />
<Route path="/view" render={()=> <DetailsComponent {...myProps} />} />

How do I pass props down from react-router-doms's render?

I am aware that to pass down props to my Routes I need to use the render. For example:
const BaseRouter = (props) => (
<div>
{console.log(props)} // this displays everything I need, including props.username
<Route exact path='/room/' render={(props) => <Room {...props} />} />
</div>
);
The problem is that inside my Room component, the only props passed down are history, location, and match (these come straight from react-router-dom).
I have also tried manually passing the props needed down:
<Route exact path='/room/' render={(props) => <Room {...props} username={props.username} />} />
But inside Room, I still get undefined for props.username. Any ideas?
You need to give a different name to you variables. Doing render={(props) => <Room {...props} /> will always pass the router props, that is why props.username is undefined.
const BaseRouter = (props) => (
<div>
<Route exact path='/room/' render={(routeProps) => <Room {...routeProps} {...props} />} />
</div>
);
Route component takes children as well.
const BaseRouter = (props) => (
<div>
{console.log(props)} // this displays everything I need, including props.username
<Route exact path='/room/'>
<Room {...props}/>
</Route>
</div>
);
In Room component you can use useHistory, useLocation and useRouteMatch hooks. If Room is class based component then withRouter HOC is also useful.

Rendering conditional route, when mapping components

I have an application, where I map some sample user components. I add some props, and I want to make a conditional nav link, that renders just a simple "profile components that show the name.
So far I have made a conditional nav, link inside the component, and the props get send correctly, and it displays the paragraph under my User component, but I want to make it redirect, so it only shows the Profile component.
Is there a way so it only shows that component. I tried with the switch but I realized, that it only renders the first, route, so everything else, will still be shown...
render() {
let persons= this.state.persons.map((item, index) =>{
return(
<Router>
<User key={index} name={item.name} img={item.img} id={item.id} />
</Router>
)
})
//user component
render(){
console.log(this.props.name)
return(
<Switch>
<div >
<img src={this.props.img} alt="profile" style={{float: 'left'}}>
</img>
<p style={{textAlign: 'center'}}>{this.props.name}</p>
<p>It's here={this.props.loggedInProp}</p>
<Route path="/:username" exact component={ Profile} />
<NavLink to={`/${this.props.name}`}>Click me</NavLink>
</div>
</Switch>
//Profile component
const Profile= ({match}) =>{
return(
<div>
<p>Hello {match.params.username}</p>
</div>
)
}
<Route
exact
path="/profile/view/:username"
render={props => <ProfileView {...props} />}
/>
inside of ProfileView component you could then use this.props.match.params.username to filter your collection of data and display only it's details.
ProfileView component
import React, { Component } from 'react';
export class ProfileView extends Component {
constructor(){
super()
this.state = {
allUsers[{ user1 ... }, {user2 ...}, ...],
selectedUser: {}
}
}
componentDidMount(){
// fetch('/get/users/from/somewhere').then(users => {
// this.setState({allUsers: users}) // Usually you would just pull your one user from the route and then fetch it's single details from a database
// })
this.setState({selectedUser: allUsers.filter(user => user.username === this.props.match.params.username)})
}
render() {
return (
<div>
<em>Do stuff with your this.state.selectedUser... things here</em>
</div>
);
}
}

Categories