I have some dynamically created Links and Routes (React Router) with path be like /details/{id} (using useParams)
When I click that link from '/' path everything works ok, but when I click it from another dynamically generated route (/details/{id}), it's just updating URL (specifically ID}, not the page's content
If I set force refresh prop on the BrowserRouter or just manually refresh the page, everything begins to work ok, but I don't want to refresh the whole page every time.
const App = () => {
return(
<BrowserRouter>
<div>
<Route path='/' exact>
<FirstComponent id={id}/>
</Route>
<Route path={`/details/:id`}>
<SecondComponent anotherId={anotherId}/>
</Route>
</div>
</BrowserRouter>
)
}
const FirstComponent = ({id}) => {
return <Link to=`/details/${id}`></Link> //renders SecondComponent
}
const SecondComponent = ({anotherId}) => {
const {id} = useParams()
//does something with id
return <Link to=`/details/${anotherId}`></Link> //Should render SecondComponent with another id but it doesn't
}
Related
When using React Router v5, it was possible to get the path (pattern) for that route using useRouteMatch.
const { path } = useRouteMatch();
React Router v6 offers a similar hook, useMatch; however this expects to receive the pattern you want to match against.
I would use the React Router v5 hook to generate routes by combining the current path with known params.
As an example, if I am on the product page of an eCommerce application (let's assume /en/product/:id) and there are links to related products (/en/product/1, /en/product/2...etc), I would previously have been able to do:
const MyComponent = () => {
const { path } = useRouteMatch();
return (
<div>
<Link to={generatePath(path, { id: 1 })}>Related Product 1</Link>
<Link to={generatePath(path, { id: 2 })}>Related Product 2</Link>
</div>
);
};
Since /en/product comes from an API and is not declared anywhere in the code, I would want the URLs to be updated based on the current path. If the user is on /es/producto, then the links would automatically be updated to /es/producto/1.
I have seen solutions on SO where it was suggested to use matchRoutes, however it feels highly inefficient, especially since the routes are dynamically generated from an external API.
const useCurrentPath = () => {
const location = useLocation()
const [{ route }] = matchRoutes(routes, location)
return route.path
}
I have created a small demo to illustrate how this used to work:
Code Sandbox Demo
In react-router-dom#6 there is no replacement for the v5 useRouteMatch hook. The links and routes no longer need to be concerned with the path pattern so much as they can simply use relative routes and links. Instead of trying to access a route's path pattern it can simply navigate relative to the currently matched path.
Example:
This navigates from "/route-a/2" to "/route-a/2/../1" or rather "/route-a/1".
const { pathname } = useLocation();
// navigate to sibling route path
<Link to={`${pathname}/../${RELATED_ID}`}>
Go to Nested Route {RELATED_ID}
</Link>
Demo
Fullcode:
import {
BrowserRouter,
Link,
Route,
Routes,
useParams,
useLocation
} from "react-router-dom";
import "./styles.css";
const RELATED_ID = 1;
const MyComponent = ({ title }) => {
const { pathname } = useLocation();
const { id } = useParams();
return (
<div>
<h1>
{title} {id}
</h1>
<pre>{pathname}</pre>
{id && RELATED_ID && (
<Link to={`${pathname}/../${RELATED_ID}`}>
Go to Nested Route {RELATED_ID}
</Link>
)}
</div>
);
};
export default function App() {
return (
<div className="app">
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/route-a">Route A</Link>
<Link to="/route-b">Route B</Link>
<Link to="/route-a/1">Nested Route A1</Link>
<Link to="/route-a/2">Nested Route A2</Link>
<Link to="/route-b/1">Nested Route B1</Link>
<Link to="/route-b/2">Nested Route B2</Link>
</nav>
<Routes>
<Route
path="/route-b"
element={<MyComponent title="Nested Route" />}
/>
<Route
path="/route-a"
element={<MyComponent title="Nested Route" />}
/>
<Route
path="/route-b/:id"
element={<MyComponent title="Nested Route" />}
/>
<Route
path="/route-a/:id"
element={<MyComponent title="Nested Route" />}
/>
<Route path="/" />
</Routes>
</BrowserRouter>
</div>
);
}
In react-router-dom v5
// Receive the matched url of the current <Route/>
const { url } = useRouteMatch();
In react-router-dom v6
const url = useResolvedPath("").pathname;
I'm using the following versions:
`"react-router": "^5.2.0",`
`"react-router-domreact-router": "^5.2.0",`
Not sure if my current setup is React-router 5 friendly or not, I was using a version prior to v5 before this.
The problem in this example is with <Route component={withTracker(InterviewContainer)} path="/interviews/companies/:companyId" /> and <Link/>
Here's my scenario:
Home page loads with a list of company links
Click on a company <Link /> which routes me to /interviews/companies/:companyId
Page loads fine, I see images, etc. for that particular company
Click browser's Back button
Click on a different company <Link /> that points to a different companyId
Problem: for #5, when the company page initially loads, it's loading with stale images and data for some reason. So in other words, I'm seeing the previous company's data & images from step #2 briefly until my React hook makes a new call to get data for this new CompanyId and repaints the browser with the right data (data for the companyId represented in the new route)
index.tsx (note the use of BrowserRouter here)
import { BrowserRouter as Router } from 'react-router-dom';
//...more code and then:
render(
<>
<div className="Site">
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>
</div>
<Footer />
</>,
);
App.ts
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
...more code and then here are my routes:
<Switch>
<Route component={withTracker(HomePageContainer)} exact path="/" />
<Route
path="/companies/:companyId/details"
render={(props: RouteComponentProps<{ companyId: string }>) => (
<CompanyDetailContainer {...props} fetchCompanyNew={fetchCompanyNew} httpRequest={Request} useFetchCompany={useFetchCompany} />
)}
/>
<Route component={withTracker(InterviewContainer)} path="/interviews/companies/:companyId" />
<Route component={withTracker(About)} path="/about" />
<Route component={withTracker(Container)} path="/" />
<Route component={withTracker(NotFound)} path="*" />
</Switch>
Here is how the company Link is coded:
Note: I am using Redux State
"react-redux": "^7.2.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
InterviewContainer.tsx (the parent that does the company fetching)
class InterviewContainer extends Component<PropsFromRedux & RouteComponentProps<{ companyId: string }>> {
componentDidMount() {
const { fetchCompany } = this.props;
const { companyId } = this.props.match.params;
fetchCompany(companyId);
}
render() {
const { company } = this.props;
return (company && <Interview className="ft-interview" company={company} />) || null;
}
}
const mapState = (state: RootState) => ({
company: state.company.company,
});
const mapDispatch = {
fetchCompany: fetchCompanyFromJSON,
};
const connector = connect(mapState, mapDispatch);
type PropsFromRedux = ConnectedProps<typeof connector>;
export default withRouter(connect(mapState, mapDispatch)(InterviewContainer));
LinkItem.tsx (one of the children rendered by InterviewContainer and receives the company from InterviewContainer)
render() {
const { company } = this.props,
uri = company.notInterviewed ? `companies/${company.id}/details` : `/interviews/companies/${company.id}`,
className = `margin-top-10 margin-bottom-10 ${company.notInterviewed ? 'ft-company-not-interviewed' : ''}`;
const link = (
<Link className={className} id={company.id.toString()} to={uri}>
<span id="company-name">{company.name}</span>
</Link>
);
}
I think I may have to reset Redux state on route change. I see people in the past have used LOCATION_CHANGE but that's outdated and that's a constant provided by third party redux libs that are no longer supported. So not sure how to do that with Redux v7+
So I think I just need a way to detect a location change and then somehow update my react store to reset company (set company: state.company.company, to undefined from my redux action)
I know things like this can be cumbersome. Have you tried passing in state with the Link as <Link to={uri} state={...someState} />. Then wherever it is loading it should rerender or reset props according to that. Maybe throw some skeleton loaders or conditional rendering logic.
I'm thoroughly lost and would like to ask for a recomendation on how to implement browser history inside my app.
With Router, all i have is a single component which gets assigned pages based on which page i'm on. pages and text inside app is acquired from an api, and whenever i click a button, the api gets called again.
<Router>
<Switch>
<Route to="/" component={Body} />
...
which probably doesnt even work as it should, because the Link tags are on the buttons, and they point to /page/number:
const renderPageNumbers = apiPagingSliced.map((links, index) => {
return <Link key={index} to={`/page/${links.label}`}>
<button key={index} id={links.label}
onClick={props.handleClick}
className={(links.active ? "mark-page" : "") + " " + (links.url === null ? "remove-btn" : "")}
>{links.label}
</button></Link>
}
)
i've managed to get it working so that i get "www.webpage.com/page/3" for example. But when i press back in browser, it only changes the url into previous page, doesn't do anything else. How do i implement a functional back/forward history function?
First you should add the route params example : "/:id"
<Route to="/some_page/:id" component={SomePage} />
Then import useHistory and UseParams from react-router :
import { useHistory, useParams } from "react-router-dom";
let { id } = useParams();
let history = useHistory();
<button onClick={() => history.push(`/some_page/${id}`)}> Go to page </button>
First you should add a route to the /page/(some number)
And this is done like this:
<Router>
<Switch>
<Route to="/" component={Body} />
<Route to="/page/:id" component={Page} />
...
And now in the Page component import a react router dom hook called useParams
import { useHistory } from 'react-router-dom'
const Page = () => {
const history = useHistory()
//pageID will be equal to the page number
return (
<div>
<button onClick={event => history.goBack`}>Go back!</button>
</div>
)
}
Let's say I have the following routes set up (just an example, actual routes aren't this messy):
<Router>
<Switch>
<Route path="/app/fun/:userid/profile" component={Profile} exact/>
<Route path="/photos/:userid" component={Photos} exact/>
<Route path="/:userid/contact" component={Contact} exact/>
</Switch>
</Router>
From ANY of the pages above, how can I link to the SAME page, but with a different userid?
eg:
/**
* Shared component that's rendered by Profile, Photos and Contact
*/
class SharedComponent extends React.Component {
render() {
const bobId = 423423432;
return (
<div>
<Link to={/* what to put here? */}>
Redirect to the same page but with Bob's userid
</Link>
</div>
);
}
}
export default withRouter(SharedComponent);
Note: using react router v4
Actually, I found a way to do this. Not sure if it's the best way, but it works:
/**
* Shared component that's rendered by Profile, Photos and Contact
*/
class SharedComponent extends React.Component {
render() {
const { location, match } = this.props;
const bobId = 423423432;
const bobUrl = location.pathname.replace(match.params.userid, bobId);
return (
<div>
<Link to={bobUrl}>
Redirect to the same page but with Bob's userid
</Link>
</div>
);
}
}
export default withRouter(SharedComponent);
Essentially, I'm taking the current url (location.pathname) and replacing the current userid (match.params.userid) with the new id (bobId)
Pass userId with path props to shared component like below,
class Profile extends React.Component {
render() {
const bobId = 423423432; // Pass bobId with path from parent to child
return (
<SharedComponent path={"/stuff1/stuff2/`${bobId}`/profile"} />
);
}
}
class SharedComponent extends React.Component {
render() {
return (
<div>
<Link to={this.props.path}>
Redirect to the same page but with Bob's userid
</Link>
</div>
);
}
}
export default withRouter(SharedComponent);
I am trying to implement React Router Breadcrumbs for v4
Following are my routes:
const routes = {
'/': 'Home',
'/page1': 'Page 1',
'/page2': 'Page 2'
};
I could put the breadcrumbs using this library in my application, however I am having following questions:
Que. #1:
When I click on Home in my breadcrumbs, I can see the URL changes to http://localhost:8080 However, browser still shows the same page I am on.
Que. #2:
When I navigate to Page2 from Page1, url changes from http://localhost:8080/page1 to http://localhost:8080/page2.
So the breadcrumbs shown changes to Home / Page 2 instead of changing like Home / Page 1 / Page 2
I know this may be because the url just has /page2 after hostname. But, can I achieve the display like: Home / Page 1 / Page 2?
Below is the code in my main App.jsx:
<Router>
<div>
<Link to="/"><div className="routerStyle"><Glyphicon glyph="home" /></div></Link>
<Route exact path="/" component={LandingPage}/>
<Route path="/page1" component={Page1}/>
<Route path="/page2" component={Page2}/>
</div>
</Router>
and if I use like belowto cater for breadcrumbs, then my page2 gets rendered below page1 stuff:
<Router>
<div>
<Link to="/"><div className="routerStyle"><Glyphicon glyph="home" /></div></Link>
<Route exact path="/" component={LandingPage}/>
<Route path="/page1" component={Page1}/>
<Route path="/page1/page2" component={Page2}/>
</div>
</Router>
Answer:
Que. #1: No need to wrap <Breadcrumbs ..../> element inside <Router> element inside each Component of application. This may be because, inclusion of <Router> element inside each Component leads to "nesting" of Router elements (note we have Router tag in landing page as well); which does not work with react router v4.
Que. #2: Refer to answer formally marked here (answered by palsrealm below)
Your breadcrumbs are based on links and they work as designed. To display the pages, you need to set up a Switch with Routes in it which would load the appropriate components when the path changes. Something like
<Switch>
<Route path='/' component={Home}/>
<Route path='/page1' component={Page1}/>
<Route path='/page2' component={Page2}/>
</Switch>
If you want the breadcrumb to show Home/Page1/Page2 your routes should be '/page1/page2' : 'Page 2'. The Route should also change accordingly.
Edit: Your Router should be
<Router>
<div>
<Link to="/"><div className="routerStyle"><Glyphicon glyph="home" /></div></Link>
<Switch>
<Route exact path="/" component={LandingPage}/>
<Route exact path="/page1" component={Page1}/>
<Route path="/page1/page2" component={Page2}/>
</Switch>
</div>
</Router>
This can also be accomplished with a HOC which would allow you to use a route config object to set breadcrumbs instead. I've open-sourced it here, but the source code is below as well:
Breadcrumbs.jsx
import React from 'react';
import { NavLink } from 'react-router-dom';
import { withBreadcrumbs } from 'withBreadcrumbs';
const UserBreadcrumb = ({ match }) =>
<span>{match.params.userId}</span>; // use match param userId to fetch/display user name
const routes = [
{ path: 'users', breadcrumb: 'Users' },
{ path: 'users/:userId', breadcrumb: UserBreadcrumb},
{ path: 'something-else', breadcrumb: ':)' },
];
const Breadcrumbs = ({ breadcrumbs }) => (
<div>
{breadcrumbs.map(({ breadcrumb, path, match }) => (
<span key={path}>
<NavLink to={match.url}>
{breadcrumb}
</NavLink>
<span>/</span>
</span>
))}
</div>
);
export default withBreadcrumbs(routes)(Breadcrumbs);
withBreadcrumbs.js
import React from 'react';
import { matchPath, withRouter } from 'react-router';
const renderer = ({ breadcrumb, match }) => {
if (typeof breadcrumb === 'function') { return breadcrumb({ match }); }
return breadcrumb;
};
export const getBreadcrumbs = ({ routes, pathname }) => {
const matches = [];
pathname
.replace(/\/$/, '')
.split('/')
.reduce((previous, current) => {
const pathSection = `${previous}/${current}`;
let breadcrumbMatch;
routes.some(({ breadcrumb, path }) => {
const match = matchPath(pathSection, { exact: true, path });
if (match) {
breadcrumbMatch = {
breadcrumb: renderer({ breadcrumb, match }),
path,
match,
};
return true;
}
return false;
});
if (breadcrumbMatch) {
matches.push(breadcrumbMatch);
}
return pathSection;
});
return matches;
};
export const withBreadcrumbs = routes => Component => withRouter(props => (
<Component
{...props}
breadcrumbs={
getBreadcrumbs({
pathname: props.location.pathname,
routes,
})
}
/>
));
The following component should return a breadcrumb at any depth, except on the home page (for obvious reasons). You won't need React Router Breadcrumb. My first public contribution, so if I'm missing an essential part, it would be great if somebody could point it out. I added » for crumbs splits, but you can obviously update that to match what you need.
import React from 'react'
import ReactDOM from 'react-dom'
import { Route, Link } from 'react-router-dom'
// styles
require('./styles/_breadcrumbs.scss')
// replace underscores with spaces in path names
const formatLeafName = leaf => leaf.replace('_', ' ')
// create a path based on the leaf position in the branch
const formatPath = (branch, index) => branch.slice(0, index + 1).join('/')
// output the individual breadcrumb links
const BreadCrumb = props => {
const { leaf, index, branch } = props,
leafPath = formatPath(branch, index),
leafName = index == 0 ? 'home' : formatLeafName(leaf),
leafItem =
index + 1 < branch.length
? <li className="breadcrumbs__crumb">
<Link to={leafPath}>{leafName}</Link>
<span className="separator">»</span>
</li>
: <li className="breadcrumbs__crumb">{leafName}</li>
// the slug doesn't need a link or a separator, so we output just the leaf name
return leafItem
}
const BreadCrumbList = props => {
const path = props.match.url,
listItems =
// make sure we're not home (home return '/' on url)
path.length > 1
&& path
// create an array of leaf names
.split('/')
// send our new array to BreadCrumb for formating
.map((leaf, index, branch) =>
<BreadCrumb leaf={leaf} index={index} branch={branch} key={index} />
)
// listItem will exist anywhere but home
return listItems && <ul className="breadcrumbs">{listItems}</ul>
}
const BreadCrumbs = props =>
<Route path="/*" render={({ match }) => <BreadCrumbList match={match} />} />
export default BreadCrumbs