How To Pass Props To Function Component use react-router-dom v6 - javascript

I want to pass props into a function from the route, my code currently looks like this
Route
<Route path="/person" render={(params) => <ProductDetails {...params} />} />
Code
function ProductDetails(props) {
return (
<div className="section has-text-centered mt-4">
console.log(props)
<h1>Hello</h1>
</div>
);
};
export default ProductDetails;
What am I missing here, please help.

In react-router-dom v6 you pass props to the routed component directly since they are passed as a ReactElement (a.k.a. JSX).
Example:
<Route path="person" element={<ProductDetails /* pass props here */ />} />
Based on your example snippet though, it sounds like your question it more specifically about passing the "route props" to a component. In RRDv6 there are no longer any route props, i.e. no history, location, and match props. Use the React hooks to access these: useNavigate for navigation, useLocation for location, and useMatch for match and useParams specifically for the route params.
import { useNavigate, useLocation, useParams } from 'react-router-dom';
function ProductDetails(props) {
const navigate = useNavigate();
const location = useLocation();
const match = useMatch();
useEffect(() => {
console.log(props);
}, [props]);
return (
<div className="section has-text-centered mt-4">
<h1>Hello</h1>
</div>
);
};
export default ProductDetails;

You can pass function with props in React Router DOM V6 in this way:
<Route path="/person" element={<ProductDetails render={(params) => ({ ...params })} />} />
And you can pass more props as names outside of render function.
Example:
<Route path="/person" element={<ProductDetails render={(params) => ({ ...params })} user={user} />} />

Related

Upgrading from react router v5 to v6

Following this tutorial series to try to build a simple React, Electron, and firebase app.
This project mirrors this demo project. I'm getting a lot of compile errors, mostly outdated content and dependencies, but managed to fix most of them up. The main thing I'm struggling with now is upgrading some code from react-router v5 to v6, specifically in app.js
import React, { useState, useEffect } from "react";
import { Router, Routes, Route, Navigate } from "react-router-dom";
import AddMoviePage from "../pages/add-movie-page";
import EditMoviePage from "../pages/edit-movie-page";
import AccountPage from "../pages/account-page";
import MoviesPage from "../pages/movies-page";
import NotFoundPage from "../pages/not-found-page";
import { auth } from "../data/firebase";
import Nav from "./nav";
import { createMemoryHistory } from "history";
function AuthenticatedRoute(props) {
const { isAuthenticated, children, ...routeProps } = props;
return <Route {...routeProps}>{isAuthenticated ? children : <Navigate to="/account" />}</Route>;
}
function App() {
const [user, setUser] = useState(null);
const isAuthenticated = user !== null;
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((currentUser) => {
setUser(currentUser);
});
return unsubscribe;
}, []);
const history = createMemoryHistory();
console.log(history);
return (
<Router history={history}>
<Nav user={user} />
<Routes>
<Route path="/account">
<AccountPage user={user} />
</Route>
<AuthenticatedRoute path="/" exact isAuthenticated={isAuthenticated}>
<MoviesPage user={user} />
</AuthenticatedRoute>
<AuthenticatedRoute path="/add" isAuthenticated={isAuthenticated}>
<AddMoviePage user={user} />
</AuthenticatedRoute>
<AuthenticatedRoute path="/edit/:id" isAuthenticated={isAuthenticated}>
<EditMoviePage user={user} />
</AuthenticatedRoute>
<Route path="*">
<NotFoundPage />
</Route>
</Routes>
</Router>
);
}
export default App;
I'm getting the following error and can't really figure out what's going on:
Uncaught TypeError: Cannot read properties of undefined (reading 'pathname')
The above error occurred in the <Router> component.
Issues
The main issue here is that you are importing and using the low-level Router component instead of one of the high-level routers (i.e. BrowserRouter, MemoryRouter, HashRouter, etc). The Router component has a couple required props and history isn't one of them.
Router Interface:
declare function Router(
props: RouterProps
): React.ReactElement | null;
interface RouterProps {
basename?: string;
children?: React.ReactNode;
location: Partial<Location> | string; // <-- required!
navigationType?: NavigationType;
navigator: Navigator; // <-- required!
static?: boolean;
}
The high-level routers all instantiate/manage a history reference internally and pass the required props and render the base Router.
Additional issues found in the code:
Another issue is that in react-router-dom#6 custom route components are no longer valid. Only Route components can be rendered by the Routes component. You'll instead convert your older v5 custom route components, a.k.a. AuthenticatedRoute, either into Wrapper components that render the children prop, or as the preferred method a Layout Route.
A final related issue is that Route components and only be rendered by the Routes component or other Route components in the case of building nested routes. In other words, the only valid children components of a Route component is another Route component. The routed content you want to be rendered on a route is passed to the Route component's element prop.
Solution
Convert AuthenticatedRoute to a layout route.
import { Navigate, Outlet } from 'react-router-dom';
function AuthenticatedRoute({ isAuthenticated }) {
if (isAuthenticated === undefined) {
// Don't render the protected content or redirect until we confirm
// authentication status.
return null; // or loading indicator/spinner/etc
}
return isAuthenticated ? <Outlet /> : <Navigate to="/account" replace />;
}
It seems you are wanting to really use a MemoryRouter since you are instantiating your own MemoryHistory object. Import and render the MemoryRouter directly. Move the route "children" onto their respective route's element prop.
Example:
...
import {
MemoryRouter as Router, // <-- import high-level router
Routes,
Route,
Navigate,
Outlet
} from "react-router-dom";
...
function AuthenticatedRoute({ isAuthenticated }) {
if (isAuthenticated === undefined) {
return null; // or loading indicator/spinner/etc
}
return isAuthenticated ? <Outlet /> : <Navigate to="/account" replace />;
}
function App() {
const [user, setUser] = useState(); // <-- initially not auth'd or unauth'd
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((currentUser) => {
setUser(currentUser); // <-- sets to user object or null
});
return unsubscribe;
}, []);
return (
<Router> // <-- Now really a MemoryRouter
<Nav user={user} />
<Routes>
<Route path="/account" element={<AccountPage user={user} />} />
<Route element={<AuthenticatedRoute isAuthenticated={user} />}>
<Route path="/" element={<MoviesPage user={user} />} />
<Route path="/add" element={<AddMoviePage user={user} />} />
<Route path="/edit/:id" element={<EditMoviePage user={user} />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Router>
);
}
export default App;

React Router V6 Re-renders on Route Change

When I navigate back and forth between routes, React Router re-renders memoized routes causing useEffect(() => []) to re-run and data to re-fetch. I'd like to prevent that and instead keep existing routes around but hidden in the dom. I'm struggling with "how" though.
The following is sample code for the problem:
import React, { useEffect } from "react";
import { BrowserRouter as Router, Route, Routes, useNavigate } from "react-router-dom";
export default function App() {
return (
<Router>
<Routes>
<Route path={"/"} element={<MemoizedRouteA />} />
<Route path={"/b"} element={<MemoizedRouteB />} />
</Routes>
</Router>
);
}
function RouteA() {
const navigate = useNavigate()
useEffect(() => {
alert("Render Router A");
}, []);
return (
<button onClick={() => { navigate('/b') }}>Go to B</button>
);
};
const MemoizedRouteA = React.memo(RouteA)
function RouteB() {
const navigate = useNavigate()
useEffect(() => {
alert("Render Router B");
}, []);
return (
<button onClick={() => { navigate('/') }}>Go to A</button>
);
}
const MemoizedRouteB = React.memo(RouteB)
Sandbox: https://codesandbox.io/s/wonderful-hertz-w9qoip?file=/src/App.js
With the above code, you'll see that the "alert" code is called whenever you tap a button or use the browser back button.
With there being so many changes of React Router over the years I'm struggling to find a solution for this.
When I navigate back and forth between routes, React Router re-renders
memoized routes causing useEffect(() => []) to re-run and data to
re-fetch. I'd like to prevent that and instead keep existing routes
around but hidden in the dom. I'm struggling with "how" though.
Long story short, you can't. React components rerender for one of three reasons:
Local component state is updated.
Passed prop values are updated.
The parent/ancestor component updates.
The reason using the memo HOC doesn't work here though is because the Routes component only matches and renders a single Route component's element prop at-a-time. Navigating from "/" to "/b" necessarily unmounts MemoizedRouteA and mounts MemoizedRouteB, and vice versa when navigating in reverse. This is exactly how RRD is intended to work. This is how the React component lifecycle is intended to work. Memoizing a component output can't do anything for when a component is being mounted/unmounted.
If what you are really trying to minimize/reduce/avoid is duplicate asynchronous calls and data fetching/refetching upon component mounting then what I'd suggest here is to apply the Lifting State Up pattern and move the state and useEffect call into a parent/ancestor.
Here's a trivial example using an Outlet component and its provided context, but the state could be provided by any other means such as a regular React context or Redux.
import React, { useEffect, useState } from "react";
import {
BrowserRouter as Router,
Route,
Routes,
Outlet,
useNavigate,
useOutletContext
} from "react-router-dom";
export default function App() {
const [users, setUsers] = useState(0);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then(setUsers);
}, []);
return (
<Router>
<Routes>
<Route element={<Outlet context={{ users }} />}>
<Route path={"/"} element={<RouteA />} />
<Route path={"/b"} element={<RouteB />} />
</Route>
</Routes>
</Router>
);
}
function RouteA() {
const navigate = useNavigate();
return (
<div>
<button onClick={() => navigate("/b")}>Go to B</button>
</div>
);
}
function RouteB() {
const navigate = useNavigate();
const { users } = useOutletContext();
return (
<div>
<button onClick={() => navigate("/")}>Go to A</button>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} : {user.email}
</li>
))}
</ul>
</div>
);
}

Props not passing in dynamic URL - TypeError React.js

I am trying to pass props to a component, Location, using React router as a url parameter, however I am getting a type error because props.match.params.location is undefined. I am passing the location in the url Link of the Locations component and when I click on this link in the browser, it redirects to the correct url with the correct parameter:
http://localhost:3000/locations/tokyo
However this is triggering this error:
Uncaught TypeError: Cannot read properties of undefined (reading 'params')
at Location (Location.js:10:1)
App.js:
class App extends React.Component {
render() {
return (
<div className="App">
<AppNavbar />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/stock-levels" element={<StockLevels />} />
<Route path="/locations" element={<Locations />} />
<Route path="/locations/:location" element={<Location />} />
</Routes>
</div>
)
}
}
export default App;
Locations.js:
import {Link} from "react-router-dom";
function Locations() {
const locations = ["tokyo", "location2", "location3", "location4", "location5", "location6", "location7", "location8"]
const locationList = locations.map(location => {
return <div className="location-container">
<Card className="location-card">
<CardTitle><Link to={`/locations/${location}`}>{location}</Link></CardTitle>
</Card>
</div>
})
return (
<>
<h1>Locations</h1>
{locationList}
</>
)
}
export default Locations
Location.js:
function Location(props) {
//Make location equal to the location passed via route
const location = props.match.params.location
return (
<>
<h1>Location</h1>
<h2>{location}</h2>
</>
)
}
export default Location
As far as I can tell, with how the routes are configured and the link url:
<Link to={`/locations/${location}`}>
This should be passed as props.
Thanks for any help!
In react-router-dom v6 there are no longer any route props, i.e. history, location, and match, instead they are replaced by React hooks. props.match is undefined and throws an error when trying to then access props.match.params.
history object replaced by a navigate function via useNavigation
location object via useLocation
match via a useMatch hook, but params object via useParams was added in v6
The useParams hook is what you want to access the location route param.
import { useParams } from 'react-router-dom';
...
function Location() {
const { location } = useParams();
return (
<>
<h1>Location</h1>
<h2>{location}</h2>
</>
);
}
The question is a bit confising in this format, could you plase share your issue in an online sandbox?
https://codesandbox.io/ - is a goood one.
I think you need to use useParams to get access to the params.
<Routes>
<Route path="/locations/:location" element={<Location />} />
</Routes>
import React from "react"
import { useParams } from "react-router-dom"
export default function Location() {
const { location } = useParams()
return (
<>
<h1>Location</h1>
<h2>{location}</h2>
</>
)
}
Forgive the spacing.

Attempting to access link to state within another component results in a typerror where state in undefined

I am attempting to pass down state via the <Link> component as such to my NextPage component:
<Link to={{pathname: `/job-run-id/${item.job_run_id}`, state: { prevPath: location.pathname }}}
>
{item.job_run_id}
</Link>
The NextPage component is being rendered via the following route:
<Route exact path="/job-run-id/:jobID">
<NextPage />
</Route>
The NextPage component is setup as follows and just receives the props:
const NextPage = (props) => (
<div>
<p>previous path is: {props.location.state.prevPath}</p>
</div>
);
Attempting to run the above code results in the following error:
TypeError: Cannot read property 'state' of undefined
Unfortunately I can't see what I am doing wrong.
In order for the NextPage component to receive the route props (history, location, and match) it needs to be rendered by one of render, component, or children function props.
Route render methods
component
<Route
exact
path="/job-run-id/:jobID"
component={NextPage}
/>
render
<Route
exact
path="/job-run-id/:jobID"
render={routeProps => <NextPage {...routeProps} />}
/>
children function
<Route
exact
path="/job-run-id/:jobID"
children={routeProps => (
<NextPage {...routeProps} />
)}
/>
For your use case, and since you aren't passing additional props to NextPage, I suggest using the component method to render your component on the route.
An alternative if you don't want to change how NextPage is rendered is you decorate it with the withRouter Higher Order Component to have the route props injected into it. Export the decorated NextPage component.
default export withRouter(NextPage);
Or since NextPage is a function component, use the useLocation React hook to get to the route state.
import { useLocation } from 'react-router-dom';
const NextPage = () => {
const { state } = useLocation();
return (
<div>
<p>previous path is: {state.prevPath}</p>
</div>
)
};

react router slug compared to part of array

I started to experiment with react router, and dynamic matches.
I wanted to create a function which matches the slug of the URL to a slug in a JSON file.
The error I get:
TypeError: Unable to get property 'slug' of undefined or null reference
I think that the 'Slug' of the url is undefined, but I am not sure on how to fix it.
screenshot of error
my code for routes.js:
import React from 'react';
import Header from './components/header/header.js';
import Home from './components/home/home.js';
import About from './components/about/about.js';
import NotFound from './components/notFound/notFound.js'
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import PostPage from './components/postpage/postpage.js'
import posts from './files/data.json';
class Routes extends React.Component {
render(){
return (
<Router>
<div>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/:slug" component={props => {
const postt = posts.posts.filter (post => props.params.slug === post.slug)
console.log(postt.length)
return <PostPage post={postt} />
} } />
}}/>
<Route component={NotFound} />
</Switch>
</div>
</Router>
);
}
}
export default Routes;
PostsPage.js:
import React from 'react';
import Post from '../post/post.js'
const PostPage = (props) => (
<div>
<Post {...props.post}/>
</div>
);
export default PostPage;
and posts.js:
import React from 'react';
import { Link } from 'react-router-dom';
import './post.css';
class Post extends React.Component {
render(){
return(
<div>
<div >
<h2 className='subTitle'><Link to={`/post/${this.props.slug}`} className='link'>{this.props.title}</Link></h2>
<p className='content'>{this.props.excerpt}</p>
</div>
</div>
);
}
}
export default Post;
If you made it this far thank you for helping
slug variable is given inside match props which you are missing.
<Route path="/:slug" render={props => {
const postt = posts.posts.filter (post => props.match.params.slug === post.slug)
console.log(postt.length)
return <PostPage post={postt} />
} } />
}}/>
Also, do not inline component use a render function instead. From the docs:
When you use component (instead of render or children, below) the
router uses React.createElement to create a new React element from the
given component. That means if you provide an inline function to the
component prop, you would create a new component every render. This
results in the existing component unmounting and the new component
mounting instead of just updating the existing component. When using
an inline function for inline rendering, use the render or the
children prop (below).
https://reacttraining.com/react-router/web/api/Route/render-func
One of the ways you can get this fixed is by using .find() instead of .filter() like this :
const postt = posts.find (post => props.match.params.slug === post.slug)
And then inside your <Router /> make sure to send the rest of {...props} as well :
<Route path="/:slug" component={props => {
const postt = posts.find (post => props.match.params.slug === post.slug)
console.log(postt.length)
return <PostPage post={postt} {...props} />
} } />

Categories