I'm converting a React/JavaScript project into a React/TypeScript. I'm using react-router-dom to handle all of the routing stuff. So, I have a file called routes/index.js (Yes, a JavaScript file) and inside of this file you will find something like this:
import { lazy } from 'react'
// use lazy for better code splitting, a.k.a. load faster
const Dashboard = lazy(() => import('../pages/Dashboard'))
const routes = [
{
path: '/dashboard', // the url
component: Dashboard, // view rendered
},
]
export default routes
In another file, that helpme to load the routes, I have the following code:
<Switch>
{routes.map((route, i) => {
return route.component ? (
<Route
key={i}
exact={true}
path={`/app${route.path}`}
render={(props) => <route.component {...props} />}
/>
) : null;
})}
<Redirect exact from="/app" to="/app/dashboard" />
<Route component={Page404} />
</Switch>;
The problem is the <route.component {...props}/>, TypeScript give me this error:
Type '{ history: History; location: Location; match: match<{}>; staticContext?: StaticContext | undefined; }' has no properties in common with type 'IntrinsicAttributes'.
How I can fix this problem?
As far as I know, you are getting this error because of using a lowercase component name. So, you can use something like this:
const Component = route.component as React.ComponentType;
...
return <Component {...someProps} />
you can also provide some generic types for this React.ComponentType
Related
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 trying to build a dynamic router with React. The idea is that the Routes are created from the data (object) received from the backend:
Menu Object:
items: [
{
name: "dashboard",
icon: "dashboard",
placeholder: "Dashboard",
path: "/",
page: "Dashboard",
exact: true,
},
{
name: "suppliers",
icon: "suppliers",
placeholder: "Suppliers",
path: "/suppliers",
page: "Suppliers",
exact: true,
}
]
The routes hook :
export const RouteHook = () => {
// Dispatch to fetch the menu object from the backend
const dispatch = useDispatch();
dispatch(setMenu());
// Select the menu and items from Redux
const { items } = useSelector(getMainMenu);
// useState to set the routes inside
const [menuItems, setMenuItems] = useState([]);
const { pathname } = useLocation();
useEffect(() => {
// Loop trough the menu Items
for (const item of items) {
const { page, path, name, exact } = item;
// Import dynamically the route components
import(`../pages/${name}/${page}`).then((result) => {
// Set the routes inside the useState
setMenuItems(
<Route exact={exact} path={path} component={result[page]} />
);
});
}
// Check if pathname has changed to update the useEffect
}, [pathname]);
return (
<Switch>
{/* Set the routes inside the switch */}
{menuItems}
</Switch>
);
};
Now here's the problem. Not all the components load. Usually the last component loads and when clicking on a diffrent route the component won't change. Except if you got to the page and refresh (F5).
What am I missing here? Is it possible to create full dynamic routes & components in react?
I'm not sure 100% what's going on here, but here's a problem I see:
const [menuItems, setMenuItems] = useState([]);
You're saying that menuItems is an array of something. But then:
import(`../pages/${name}/${page}`).then((result) => {
// Set the routes inside the useState
setMenuItems(
<Route exact={exact} path={path} component={result[page]} />
);
});
On every iteration you are setting the menu items to be a singular Route component. Probably what you're thinking youre doing is
const routes = items.map(item => {
const { page, path, name, exact } = item;
return import(`../pages/${name}/${page}`).then((result) => {
<Route exact={exact} path={path} component={result[page]} />
});
})
setMenuItems(routes)
But this makes no sense, because your map statement is returning a Promise.then function. I'm not entirely sure why you're dynamically importing the components here. You're better off doing a simple route mapping:
const routes = items.map(item => {
const { page, path, name, exact } = item;
return <Route exact={exact} path={path} component={components[page]} />
})
setMenuItems(routes)
Where components is an object whose keys are the values of page and whose values are actual components, i.e.:
const components = {
Suppliers: RenderSuppliers,
Dashboard: RenderDashboard
}
If you want these components lazy-loaded, use react suspense:
const Suppliers = React.lazy(() => import("./Suppliers"))
const Dashboard = React.lazy(() => import("./Dashboard"))
const components = {
Suppliers,
Dashboard,
}
const routes = items.map(item => {
const { page, path, name, exact } = item;
return (
<Suspense fallback={<SomeFallbackComponent />}>
<Route
exact={exact}
path={path}
component={components[page]}
/>
</Suspense>
)
})
setMenuItems(routes)
This is just a quick review of what may be going wrong with your code, without a reproducible example, its hard to say exactly.
Seth has some great suggestions, but here's how you can clean this up while still using dynamic imports.
Hopefully you can see that you are calling setMenuItems with a single Route component instead of all of them. Each time that you setMenuItems you are overriding the previous result and that's why only the last Route actually works -- it's the only one that exists!
Your useEffect depends on the pathname which seems like you are trying to do the routing yourself. Since you are using react-router-dom you would include all of the Route components in your Switch and let the router handle the routing.
So you don't actually need any state here.
You can use the React.lazy component import helper inside of the Route. You need a Suspense provider around the whole block in order to use lazy imports.
I don't like that you use two variables in the path for a component ../pages/${name}/${page}. Why not export the component from the ./index.js of the folder?
export const Routes = () => {
// Dispatch to fetch the menu object from the backend
const dispatch = useDispatch();
dispatch(setMenu());
// Select the menu and items from Redux
const items = useSelector((state) => state.routes.items);
return (
<Suspense fallback={() => <div>Loading...</div>}>
<Switch>
{items.map(({ exact, path, name }) => (
<Route
key={name}
exact={exact}
path={path}
component={React.lazy(() => import(`../pages/${name}`))}
/>
))}
</Switch>
</Suspense>
);
};
It works!
Code Sandbox Link
I have a simple setup to test the use of the useContext hook, when you want to change the context value in child components.
A simple Context is defined in its own file like such:
import React from 'react'
const DataContext = React.createContext({})
export const DataProvider = DataContext.Provider
export default DataContext
Then I wrap my router in a provider in a component that exposes its state to use as a reference for the ContextProvider, as such:
import { DataProvider } from './dataContext.js'
export default function App(props) {
const [data, setData] = useState("Hello!")
const value = { data, setData }
const hist = createBrowserHistory();
return (
<DataProvider value={value}>
<Router history={hist}>
<Switch>
<Route path="/admin" component={Admin} />
<Redirect from="/" to="/admin/services" />
</Switch>
</Router>
</DataProvider>
)
}
Finally I have two Views that I am able to navigate between initially, one of them showcasing the context value, as well as containing a button to change it:
export default function EndpointView(props) {
const { data, setData } = useContext(DataContext)
return (
<div>
<h1>{data}!</h1>
<Button onClick={() => setData(Math.random())}>Update context state</Button>
</div>
)
}
The functionality seems to work, as the showcases text is updated.
The problem is, when I have clicked the button, I can no longer navigate in my navbar, even though the url is changing. Any ideas as to why?
This is showcased in this picture, where the url is corresponding to the top-most item in the side bar, even though we are stuck in the "endpoint view"-component.
Edit:
So the routing works by including a switch in the Admin layout:
const switchRoutes = (
<Switch>
{routes.map((prop, key) => {
if (prop.layout === "/admin") {
return (
<Route
path={prop.layout + prop.path}
component={prop.component}
key={key}
/>
);
}
return null;
})}
<Redirect from="/admin" to="/admin/services" />
</Switch>
);
Where the routes (which we .map) are fetched from another file that looks like this:
const dashboardRoutes = [
{
path: "/services",
name: "Services view",
icon: AccountBalance,
component: ServicesView,
layout: "/admin"
},
{
path: "/endpoint",
name: "Endpoint view",
icon: FlashOn,
component: EndpointView,
layout: "/admin"
}
];
export default dashboardRoutes;
I was able to solve this issue.
I suspect the problem was that updating the state reloaded the root router component which caused some issues.
Instead I moved the DataProvider tag one step down the tree, to wrap the switch in the Admin component.
Test Case
https://codesandbox.io/s/rr00y9w2wm
Steps to reproduce
Click on Topics
Click on Rendering with React
OR
Go to https://rr00y9w2wm.codesandbox.io/topics/rendering
Expected Behavior
match.params.topicId should be identical from both the parent Topics component should be the same as match.params.topicId when accessed within the Topic component
Actual Behavior
match.params.topicId when accessed within the Topic component is undefined
match.params.topicId when accessed within the Topics component is rendering
I understand from this closed issue that this is not necessarily a bug.
This requirement is super common among users who want to create a run in the mill web application where a component Topics at a parent level needs to access the match.params.paramId where paramId is a URL param that matches a nested (child) component Topic:
const Topic = ({ match }) => (
<div>
<h2>Topic ID param from Topic Components</h2>
<h3>{match.params.topicId}</h3>
</div>
);
const Topics = ({ match }) => (
<div>
<h2>Topics</h2>
<h3>{match.params.topicId || "undefined"}</h3>
<Route path={`${match.url}/:topicId`} component={Topic} />
...
</div>
);
In a generic sense, Topics could be a Drawer or Navigation Menu component and Topic could be any child component, like it is in the application I'm developing. The child component has it's own :topicId param which has it's own (let's say) <Route path="sections/:sectionId" component={Section} /> Route/Component.
Even more painful, the Navigation Menu needn't have a one-to-one relationship with the component tree. Sometimes the items at the root level of the menu (say Topics, Sections etc.) might correspond to a nested structure (Sections is only rendered under a Topic, /topics/:topicId/sections/:sectionId though it has its own normalized list that is available to the user under the title Sections in the Navigation Bar).
Therefore, when Sections is clicked, it should be highlighted, and not both Sections and Topics.
With the sectionId or sections path unavailable to the Navigation Bar component which is at the Root level of the application, it becomes necessary to write hacks like this for such a commonplace use case.
I am not an expert at all at React Router, so if anyone can venture a proper elegant solution to this use case, I would consider this to be a fruitful endeavor. And by elegant, I mean
Uses match and not history.location.pathname
Does not involve hacky approaches like manually parsing the window.location.xxx
Doesn't use this.props.location.pathname
Does not use third party libraries like path-to-regexp
Does not use query params
Other hacks/partial solutions/related questions:
React Router v4 - How to get current route?
React Router v4 global no match to nested route childs
TIA!
React-router doesn't give you the match params of any of the matched children Route , rather it gives you the params based on the current match. So if you have your Routes setup like
<Route path='/topic' component={Topics} />
and in Topics component you have a Route like
<Route path=`${match.url}/:topicId` component={Topic} />
Now if your url is /topic/topic1 which matched the inner Route but for the Topics component, the matched Route is still, /topic and hence has no params in it, which makes sense.
If you want to fetch params of the children Route matched in the topics component, you would need to make use of matchPath utility provided by React-router and test against the child route whose params you want to obtain
import { matchPath } from 'react-router'
render(){
const {users, flags, location } = this.props;
const match = matchPath(location.pathname, {
path: '/topic/:topicId',
exact: true,
strict: false
})
if(match) {
console.log(match.params.topicId);
}
return (
<div>
<Route exact path="/topic/:topicId" component={Topic} />
</div>
)
}
EDIT:
One method to get all the params at any level is to make use of context and update the params as and when they match in the context Provider.
You would need to create a wrapper around Route for it to work correctly, A typical example would look like
RouteWrapper.jsx
import React from "react";
import _ from "lodash";
import { matchPath } from "react-router-dom";
import { ParamContext } from "./ParamsContext";
import { withRouter, Route } from "react-router-dom";
class CustomRoute extends React.Component {
getMatchParams = props => {
const { location, path, exact, strict } = props || this.props;
const match = matchPath(location.pathname, {
path,
exact,
strict
});
if (match) {
console.log(match.params);
return match.params;
}
return {};
};
componentDidMount() {
const { updateParams } = this.props;
updateParams(this.getMatchParams());
}
componentDidUpdate(prevProps) {
const { updateParams, match } = this.props;
const currentParams = this.getMatchParams();
const prevParams = this.getMatchParams(prevProps);
if (!_.isEqual(currentParams, prevParams)) {
updateParams(match.params);
}
}
componentWillUnmount() {
const { updateParams } = this.props;
const matchParams = this.getMatchParams();
Object.keys(matchParams).forEach(k => (matchParams[k] = undefined));
updateParams(matchParams);
}
render() {
return <Route {...this.props} />;
}
}
const RouteWithRouter = withRouter(CustomRoute);
export default props => (
<ParamContext.Consumer>
{({ updateParams }) => {
return <RouteWithRouter updateParams={updateParams} {...props} />;
}}
</ParamContext.Consumer>
);
ParamsProvider.jsx
import React from "react";
import { ParamContext } from "./ParamsContext";
export default class ParamsProvider extends React.Component {
state = {
allParams: {}
};
updateParams = params => {
console.log({ params: JSON.stringify(params) });
this.setState(prevProps => ({
allParams: {
...prevProps.allParams,
...params
}
}));
};
render() {
return (
<ParamContext.Provider
value={{
allParams: this.state.allParams,
updateParams: this.updateParams
}}
>
{this.props.children}
</ParamContext.Provider>
);
}
}
Index.js
ReactDOM.render(
<BrowserRouter>
<ParamsProvider>
<App />
</ParamsProvider>
</BrowserRouter>,
document.getElementById("root")
);
Working DEMO
Try utilizing query parameters ? to allow the parent and child to access the current selected topic. Unfortunately, you will need to use the module qs because react-router-dom doesn't automatically parse queries (react-router v3 does).
Working example: https://codesandbox.io/s/my1ljx40r9
URL is structured like a concatenated string:
topic?topic=props-v-state
Then you would add to the query with &:
/topics/topic?topic=optimization&category=pure-components&subcategory=shouldComponentUpdate
✔ Uses match for Route URL handling
✔ Doesn't use this.props.location.pathname (uses this.props.location.search)
✔ Uses qs to parse location.search
✔ Does not involve hacky approaches
Topics.js
import React from "react";
import { Link, Route } from "react-router-dom";
import qs from "qs";
import Topic from "./Topic";
export default ({ match, location }) => {
const { topic } = qs.parse(location.search, {
ignoreQueryPrefix: true
});
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/topic?topic=rendering`}>
Rendering with React
</Link>
</li>
<li>
<Link to={`${match.url}/topic?topic=components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/topic?topic=props-v-state`}>
Props v. State
</Link>
</li>
</ul>
<h2>
Topic ID param from Topic<strong>s</strong> Components
</h2>
<h3>{topic && topic}</h3>
<Route
path={`${match.url}/:topicId`}
render={props => <Topic {...props} topic={topic} />}
/>
<Route
exact
path={match.url}
render={() => <h3>Please select a topic.</h3>}
/>
</div>
);
};
Another approach would be to create a HOC that stores params to state and children update the parent's state when its params have changed.
URL is structured like a folder tree: /topics/rendering/optimization/pure-components/shouldComponentUpdate
Working example: https://codesandbox.io/s/9joknpm9jy
✔ Uses match for Route URL handling
✔ Doesn't use this.props.location.pathname
✔ Uses lodash for object to object comparison
✔ Does not involve hacky approaches
Topics.js
import map from "lodash/map";
import React, { Fragment, Component } from "react";
import NestedRoutes from "./NestedRoutes";
import Links from "./Links";
import createPath from "./createPath";
export default class Topics extends Component {
state = {
params: "",
paths: []
};
componentDidMount = () => {
const urlPaths = [
this.props.match.url,
":topicId",
":subcategory",
":item",
":lifecycles"
];
this.setState({ paths: createPath(urlPaths) });
};
handleUrlChange = params => this.setState({ params });
showParams = params =>
!params
? null
: map(params, name => <Fragment key={name}>{name} </Fragment>);
render = () => (
<div>
<h2>Topics</h2>
<Links match={this.props.match} />
<h2>
Topic ID param from Topic<strong>s</strong> Components
</h2>
<h3>{this.state.params && this.showParams(this.state.params)}</h3>
<NestedRoutes
handleUrlChange={this.handleUrlChange}
match={this.props.match}
paths={this.state.paths}
showParams={this.showParams}
/>
</div>
);
}
NestedRoutes.js
import map from "lodash/map";
import React, { Fragment } from "react";
import { Route } from "react-router-dom";
import Topic from "./Topic";
export default ({ handleUrlChange, match, paths, showParams }) => (
<Fragment>
{map(paths, path => (
<Route
exact
key={path}
path={path}
render={props => (
<Topic
{...props}
handleUrlChange={handleUrlChange}
showParams={showParams}
/>
)}
/>
))}
<Route
exact
path={match.url}
render={() => <h3>Please select a topic.</h3>}
/>
</Fragment>
);
If you have a known set of child routes then you can use something like this:
Import {BrowserRouter as Router, Route, Switch } from 'react-router-dom'
<Router>
<Route path={`${baseUrl}/home/:expectedTag?/:expectedEvent?`} component={Parent} />
</Router>
const Parent = (props) => {
return (
<div >
<Switch>
<Route path={`${baseUrl}/home/summary`} component={ChildOne} />
<Route
path={`${baseUrl}/home/:activeTag/:activeEvent?/:activeIndex?`}
component={ChildTwo}
/>
</Switch>
<div>
)
}
In the above example Parent will get expectedTag, expectedEvent as the match params and there is no conflict with the child components and Child component will get activeTag, activeEvent, activeIndex as the parameters. Same name for params can also be used, I have tried that as well.
Try to do something like this:
<Switch>
<Route path="/auth/login/:token" render={props => <Login {...this.props} {...props}/>}/>
<Route path="/auth/login" component={Login}/>
First, the route with the parameter and after the link without parameter.
Inside my Login component I put this line of code console.log(props.match.params.token); to test and worked for me.
If you happen to use React.FC, there is a hook useRouteMatch.
For instance, parent component routes:
<div className="office-wrapper">
<div className="some-parent-stuff">
...
</div>
<div className="child-routes-wrapper">
<Switch>
<Route exact path={`/office`} component={List} />
<Route exact path={`/office/:id`} component={Alter} />
</Switch>
</div>
</div>
And in your child component:
...
import { useRouteMatch } from "react-router-dom"
...
export const Alter = (props) => {
const match = useRouteMatch()
const officeId = +match.params.id
//... rest function code
}
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