I am trying to build a simple router for my React application, which would consist of a few routes generated from an array of objects and a static 404 route. The requirement is that transitions between every route must be animated.
I am using react-router for the browser and react-transition-group.
I want to achieve something like this (stripped-down, incomplete pseudo-code):
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
{ path: "/contact", component: Contact }
];
const createRoute = (route) => {
return (
<CSSTransition className="page" timeout={300}>
<Route path={route.path} exact component={route.component} />
</CSSTransition>
);
}
<Router>
{routes.map(createRoute)}
<CSSTransition className="page" timeout={300}>
<Route component={PageNotFound} />
</CSSTransition>
</Router>
A full version of this code can be found on this Codesandbox:
https://codesandbox.io/s/react-router-switch-csstransition-catch-all-route-bug-forked-qzt9g
I need to use the <Switch> component to prevent the 404 route from showing in all the other routes of the application, but as soon as I add the <Switch>, the animations stop working.
In the example above, you will see that the animations don't work when used with <Switch>, despite following the guides from official docs of both react-router and react-transition-group.
However, they work perfectly without the use of the <Switch>, but then of course I end up with 404 route showing all the time.
Expected result:
animated transitions between all routes, those dynamically created as well as the static 404 page
Actual result:
no animations at all or animations with 404 route always showing
I have spent the entire day trying to find a solution to the problem that I encountered. Unfortunately I can't seem to find anything that would remotely help me fix the issue I'm facing, and I've searched on Google, Stack Overflow, Medium and finally I'm back here hoping someone can help me out please.
In order to have the animation working with the Switch component, you have to pass the right location with withRouter to the CSSTransition, coupled with TransitionGroup component.
I've modified you sandbox code with the following working solution:
import React from "react";
import ReactDOM from "react-dom";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import {
BrowserRouter as Router,
Route,
Switch,
Link,
withRouter
} from "react-router-dom";
import "./styles.css";
const rootElement = document.getElementById("root");
const components = {
Home: () => <div>Home page</div>,
About: () => <div>About the company</div>,
Contact: () => <div>Contact us</div>,
NotFound: () => <div>404</div>,
Menu: ({ links, setIsSwitch }) => (
<div className="menu">
{links.map(({ path, component }, key) => (
<Link key={key} to={path}>
{component}
</Link>
))}
<Link to="/404">404</Link>
</div>
)
};
const routes = [
{ path: "/", component: "Home" },
{ path: "/about", component: "About" },
{ path: "/contact", component: "Contact" }
];
const createRoutes = (routes) =>
routes.map(({ component, path }) => {
const Component = components[component];
const nodeRef = React.createRef();
return (
<Route key={path} path={path} exact>
{({ match }) => {
return (
<div ref={nodeRef} className="page">
<Component />
</div>
);
}}
</Route>
);
});
const AnimatedSwitch = withRouter(({ location }) => (
<TransitionGroup>
<CSSTransition
key={location.key}
timeout={500}
classNames="page"
unmountOnExit
>
<Switch location={location}>{createRoutes(routes)}</Switch>
</CSSTransition>
</TransitionGroup>
));
ReactDOM.render(
<React.StrictMode>
<Router>
<components.Menu links={routes} />
<AnimatedSwitch />
</Router>
</React.StrictMode>,
rootElement
);
This article explains in detail the reasoning behind it: https://latteandcode.medium.com/react-how-to-animate-transitions-between-react-router-routes-7f9cb7f5636a.
By the way withRouter is deprecated in react-router v6.
So you should implement that hook in your own.
import { useHistory } from 'react-router-dom';
export const withRouter = (Component) => {
const Wrapper = (props) => {
const history = useHistory();
return (
<Component
history={history}
{...props}
/>
);
};
return Wrapper;
};
See Deprecated issue discussion on GitHub: https://github.com/remix-run/react-router/issues/7156.
A quick fix is you can do following
<Switch>
<Route
path={"/:page(|about|contact)"}
render={() => createRoutes(routes)}
/>
<Route>
<div className="page">
<components.NotFound />
</div>
</Route>
</Switch>
Obviously not the most elegant nor scalable solution. But doable for a small react site.
Here's the forked codesandbox of working code.
https://codesandbox.io/s/react-router-switch-csstransition-catch-all-route-bug-forked-dhv39
EDIT: I was checking router.location.key first, but reaching through address bar was causing 404 page to render every page, so this is safer.
You can pass router as props to pages and conditionally render 404 page by checking router.location.pathname
export const routes = [
...all of your routes,
{
path: "*",
Component: NotFound,
},
]
{routes.map(({ path, pageTitle, Component }) => (
<Route key={path} exact path={path}>
{router => (
<CSSTransition
in={router.match !== null}
timeout={400}
classNames="page"
unmountOnExit
>
<div className="page">
<Component router={router} />
</div>
</CSSTransition>
)}
</Route>
))}
And in your 404 component, you need to check the path of the current route if it is available around all routes:
import { routes } from "../Routes";
const checkIfRouteIsValid = path => routes.findIndex(route => route.path === path);
export default function NotFound(props) {
const pathname = props.router.location.pathname;
const [show, setShow] = useState(false);
useEffect(() => {
setShow(checkIfRouteIsValid(pathname) === -1);
}, [pathname]);
return (
show && (
<div>Not found!</div>
)
);
}
Related
Following up from my question React router v6 and relative links from page within route, I'm trying to refactor the routes in our app to be more nested.
Trouble is that it doesn't seem possible to render a Route element recursively from data, because react-router insists that Route is directly inside Route and not wrapped in another component, and I cannot see how to render recursively (to arbitrary depth) any other way.
Reproduction on codesandbox.
import React from "react";
import { BrowserRouter, Routes, Route} from "react-router-dom";
import "./styles.css";
function GenericPage() {
return <div className="page">Generic page</div>;
}
const nav = {
slug: "",
title: "Home",
children: [
{
slug: "foo",
title: "Foo"
},
{
slug: "bar",
title: "Bar"
}
]
};
const RecursiveRoute = ({ node }) => {
return (
<Route path={node.slug} element={<GenericPage />}>
{node.children?.map((child) => (
<RecursiveRoute node={child} />
))}
</Route>
);
};
export default function App() {
return (
<BrowserRouter>
<Routes>
<RecursiveRoute node={nav} />
</Routes>
</BrowserRouter>
);
}
Error from react-router:
[RecursiveRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
Issue
As the error indicates, you can't render Route components directly, they must be rendered directly by a Routes component, or another Route component in the case of nested routes.
Solution
Refactor RecursiveRoute to render a Routes component with a route for the current node and then map the node's children to routes that render the RecursiveRoute as an element.
Example:
function GenericPage({ title }) {
return (
<div className="page">
{title} page
</div>
);
}
const RecursiveRoute = ({ node }) => (
<Routes>
<Route
path={`${node.slug}/*`}
element={<GenericPage title={node.title} />}
/>
{node.children?.map((child) => (
<Route
key={child.slug}
element={<RecursiveRoute key={child.slug} node={child} />}
/>
))}
</Routes>
);
Suggestion
I strongly suggest not trying to roll your own custom route configuration and renderer, use the useRoutes hook instead to do all the heavy lifting for you.
Example:
Refactor the navigation config:
const nav = [
{
path: "/",
element: <GenericPage title="Home" />,
children: [
{
path: "foo",
element: <GenericPage title="Foo" />
},
{
path: "bar",
element: <GenericPage title="Bar" />
}
]
}
];
Pass the config to the useRoutes hook and render the result:
const routes = useRoutes(nav);
...
return routes;
Demo
I had the same puzzle to solve. In general I solve it by passing a function in Routes component. Here is my solution with few code snippets.
// in Routes.ts
interface IRoutes {
path: string
component: JSX.Element
children?: IRoutes[]
}
const routes: IRoutes[] = [
{
path: 'warehouse'
component: <WarehousePage />
children: [
{
path: 'products'
component: <ProductsPage />
},
{
path: 'units'
component: <UnitsPage />
},
]
},
]
// in AppRouter.tsx
const renderRoutesRecursive = (routes: IRoutes[]) =>
routes.map((route, index) =>
route.children ? (
renderRoutesRecursive(route.children)
) : (
<Route
key={index}
path={route.path}
element={route.component}
/>
),
)
const renderRoutes = useMemo(() => renderRoutesRecursive(routes), [routes])
return (
<Routes>
<Route path='/' element={<Layout />}>
{renderRoutes}
</Route>
</Routes>
)
// in Layout.tsx
const Layout = () => {
return (
<>
<Header />
<Navigation />
<Main>
<Outlet />
</Main>
<Footer />
</>
)
}
I have a list of pageNames and a list of pageUrls. I would like to create a router that generates a react component for each page in the list and uses a navbar with router links. I think the problem lies in the forEach function but I am not sure.
Can anyone tell what I am doing wrong?
Although I didn't find it helpful, here is a similar question.
import React, { useState } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
function App() {
const routes = [
{
name: 'home',
path: '/'
},
{
name: 'blog',
path: '/blog'
}
]
// translate (map) your array of objects into jsx
const routeComponents = routes.map(({name, path}) => ((
<Route key={name} path={path}>
<h1>{name}</h1>
</Route>
)))
return (
<div className="App" >
<Router>
<Navbar routes = {routes} />
<Switch>
{routeComponents}
</Switch>
</Router>
</div>
);
}
export default App;
function Navbar(props){
const links = props.routes.map(({name, path}) => (
<Link key={name} to={path}>{name}</Link>
))
return(
<div style={{justifyContent: "space-around", margin:"auto", display:"flex",justifyContent:"space-around",backgroundColor:"red"}}>
{links}
</div>
)
}
The issue you likely are seeing is due to the way the Switch component works. It renders the first matching Route or Redirect component. This means that within the Switch component path order and specificity matters! The home path "/" matches any path, so will always be matched and rendered.
You can avoid this by reordering the routes from more specific to less specific, or since you allude to wanting the routes to be dynamically generated from user interaction and stored in a state, specify the exact prop for all routes so all matching occurs exactly and the order and specificity is negated.
function App() {
const routes = [
{
name: 'home',
path: '/',
},
{
name: 'blog',
path: '/blog',
},
];
const routeComponents = routes.map(({name, path}) => (
<Route key={name} exact path={path}> // <-- exact prop to exactly match paths
<h1>{name}</h1>
</Route>
));
return (
<div className="App" >
<Router>
<Navbar routes={routes} />
<Switch>
{routeComponents}
</Switch>
</Router>
</div>
);
}
You're correct. forEach is populating your routes variable, which is an array. What you want is to build some JSX. This is traditionally done via map. You also do not need the useState calls, and you'll want to combine your page names and urls into a single object for easier management.
Here's some pseudocode that will put you on the path...
const routes = [
{
name: 'hello',
path: '/'
},
{
name: 'there',
path: '/there'
}
]
// translate (map) your array of objects into jsx
const routeComponents = routes.map(({name, path}) => (
<Route key={name} path={path}/>
))
// routeComponents is JSX. You originally had an array of JSX
<Switch>
{routeComponents}
</Switch>
// Same thing for your links
const navbar = routes.map(({name, path}) => (
<Link key={name} to={path}>{name}</Link>
))
<div>
{navbar}
</div>
update
some tweaks in above example to clarify it.
Update #2
Here's full code (jammed into 1 file).
Updated to react router v6.
test it here
import "./styles.css";
import React, { useState } from "react";
import {
BrowserRouter as Router,
Routes,
Route,
Link,
Outlet
} from "react-router-dom";
function Navbar({ routes }) {
const links = routes.map(({ name, path }) => (
<Link key={name} to={path}>
{name}
</Link>
));
return (
<div
style={{
justifyContent: "space-around",
margin: "auto",
display: "flex",
justifyContent: "space-around",
backgroundColor: "red"
}}
>
{links}
</div>
);
}
export default function App() {
const routes = [
{
name: "home",
path: "/"
},
{
name: "blog",
path: "/blog"
},
{
name: "about",
path: "/about"
}
];
// translate (map) your array of objects into jsx
const routeComponents = routes.map(({ name, path }) => (
<Route key={name} path={path} element={<h1>{name}</h1>} />
));
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Router>
<Routes>{routeComponents}</Routes>
<Navbar routes={routes} />
</Router>
{Outlet}
</div>
);
}
I'm testing if my components render with Redux successfully with React Testing Library. I'm having trouble having my utility component to pass the renderWithRedux test. This is my App component.
function App() {
return (
<>
<Router>
<NavBar />
<div className="container">
<Switch>
<Route exact path='/' component={Home}/>
<AuthRoute exact path='/login' component={Login} />
<AuthRoute exact path='/signup' component={Signup} />
<Route exact path='/users/:handle' component={UserProfile} />
<Route exact path='/users/:handle/post/:postId' component={UserProfile} />
</Switch>
</div>
</Router>
</>
);
}
Here is my AuthRoute utility component.
const AuthRoute = ({ component: Component, authenticated, ...rest }) => (
// if authenticated, redirect to homepage, otherwise redirect to signup or login
<Route
{...rest}
render={(props) =>
authenticated === true ? <Redirect to='/' /> : <Component {...props} />
}
/>
);
AuthRoute.test.js
const renderWithRedux = () => render(
<Provider store={myStore}>
<AuthRoute />
</Provider>
);
it('renders with Redux', () => {
const {} = renderWithRedux(<AuthRoute />);
});
I've attempted the solutions from Invariant failed: You should not use <Route> outside a <Router>, but to no avail. I appreciate any help, thank you.
Render the component under test into a router
import { MemoryRouter } from 'react-router-dom';
const renderWithRedux = ({ children }) => render(
<Provider store={myStore}>
{children}
</Provider>
);
it('renders with Redux', () => {
const {} = renderWithRedux(
<MemoryRouter>
<AuthRoute />
</MemoryRouter>
);
});
Just like the Provider to wrap redux things you have to wrap your components with routes using MemoryRouter for the tests.
import { MemoryRouter } from 'react-router';
Basically, you have two wrapper elements. It should go something like this, for example, renderWithReduxWrapp => renderWithRouter => YourTestingComponent.
I had a similar issue when trying to test Button render (which has a Link) depending on props, and was able to solve it by creating some helper functions.
Here is the example:
This is the main component, UserCard.js, which renders user data from redux, and only shows a button if withButton props is passed.
import React from "react";
import { Link } from "react-router-dom";
import { Button } from "react-bootstrap";
const CardComponent = ({ withButton }) => {
const userInfo = useSelector((state) => getUserSelector(state));
return (
<div>
<div>{userInfo}</div>
{withButton && (
<Link to="/settings" className="button-link">
<Button block>EDIT CONTACT INFO</Button>
</Link>
)}
</div>
);
};
export default CardComponent;
This is a CardComponent.test.js file.
First, you need to add these lines of code
const ReduxWrapper = ({ children }) => {
<Provider store={store}>{children} </Provider>;
}
const AppWrapper = ({ children }) => (
<BrowserRouter>
<ReduxWrapper>{children}</ReduxWrapper>
</BrowserRouter>
);
const renderWithRouter = (ui, { route = '/' } = {}) => {
window.history.pushState({}, 'Test page', route);
return render(ui, { wrapper: AppWrapper });
};
After that, you need to start your test with renderWithRouter instead of just render method.
it('should render settings button if prop withButton is passed', () => {
renderWithRouter(<CardComponent withButton />, { wrapper: ReduxWrapper });
// apply you code here. I only needed to check if the button is renederd or not.
const settingsButton = screen.queryByText(/edit contact info/i);
expect(settingsButton).toBeInTheDocument();
});
Codesandbox
Hi, if I create an array of routes with React Router which has the HOC of <MainRoute /> which wraps the actual <Component /> with <Main/>, on every path change <Main/> is getting remounted and hence the useEffect hook is getting recalled.
Is there a way to only remount the <Component /> not <Main/> itself? Maybe some kind of memoization?
Mapping over routes is very convenient, however it seems to re-map everything on the location change which doesn't happen if I just hardcode every route like <MainRoute path=.. component=.. /> inside the <Switch />.
Help highly appreciated,
Cheers
import React from "react";
import { Route, Switch, BrowserRouter, Link } from "react-router-dom";
import styled from "styled-components";
const Layout = styled.div`
margin: auto;
width: 0;
`;
const Main = ({ children, ...props }) => {
React.useEffect(() => {
console.log("API REQUEST - Called every time");
}, []);
return <Layout>{React.cloneElement(children, props)}</Layout>;
};
const MainRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props => {
return (
<Main path={rest.path}>
<Component {...props} />
</Main>
);
}}
/>
);
};
export default function App() {
return (
<BrowserRouter>
<Switch>
{routes.map(({ path, component }) => (
<MainRoute key={path} path={path} component={component} exact />
))}
</Switch>
</BrowserRouter>
);
}
const Home = () => (
<>
Home <Link to="/other">Other</Link>
</>
);
const Other = () => (
<>
Other <Link to="/">Home</Link>
</>
);
const routes = [
{ path: "/", component: Home },
{ path: "/other", component: Other }
];
I'm looking for a way to modify the page title when React-Router v4+ changes locations. I used to listen for a location change action in Redux and check that route against a metaData object.
When using React-Router v4+, there's no fixed routes list. In fact, various components around the site could use Route with the same path string. That means old method I used won't work anymore.
Is there a way I can update the page title by calling actions when certain major routes are changed or is there a better a better method to update the site's metadata?
<Route /> components have render property. So you can modify the page title when location changes by declaring your routes like that:
<Route
exact
path="/"
render={props => (
<Page {...props} component={Index} title="Index Page" />
)}
/>
<Route
path="/about"
render={props => (
<Page {...props} component={About} title="About Page" />
)}
/>
In Page component you can set the route title:
import React from "react"
/*
* Component which serves the purpose of a "root route component".
*/
class Page extends React.Component {
/**
* Here, we define a react lifecycle method that gets executed each time
* our component is mounted to the DOM, which is exactly what we want in this case
*/
componentDidMount() {
document.title = this.props.title
}
/**
* Here, we use a component prop to render
* a component, as specified in route configuration
*/
render() {
const PageComponent = this.props.component
return (
<PageComponent />
)
}
}
export default Page
Update 1 Aug 2019. This only works with react-router >= 4.x. Thanks to #supremebeing7
Updated answer using React Hooks:
You can specify the title of any route using the component below, which is built by using useEffect.
import { useEffect } from "react";
const Page = (props) => {
useEffect(() => {
document.title = props.title || "";
}, [props.title]);
return props.children;
};
export default Page;
And then use Page in the render prop of a route:
<Route
path="/about"
render={(props) => (
<Page title="Index">
<Index {...props} />
</Page>
)}
/>
<Route
path="/profile"
render={(props) => (
<Page title="Profile">
<Profile {...props} />
</Page>
)}
/>
In your componentDidMount() method do this for every page
componentDidMount() {
document.title = 'Your page title here';
}
This will change your page title, do the above mentioned for every route.
Also if it is more then just the title part, check react-helmet It is a very neat library for this, and handles some nice edge cases as well.
Picking up from the excellent answer of phen0menon, why not extend Route instead of React.Component?
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
import PropTypes from 'prop-types';
export const Page = ({ title, ...rest }) => {
useEffect(() => {
document.title = title;
}, [title]);
return <Route {...rest} />;
};
This will remove overhead code as seen below:
// old:
<Route
exact
path="/"
render={props => (
<Page {...props} component={Index} title="Index Page" />
)}
/>
// improvement:
<Page
exact
path="/"
component={Index}
title="Index Page"
/>
Update: another way to do it is with a custom hook:
import { useEffect } from 'react';
/** Hook for changing title */
export const useTitle = title => {
useEffect(() => {
const oldTitle = document.title;
title && (document.title = title);
// following line is optional, but will reset title when component unmounts
return () => document.title = oldTitle;
}, [title]);
};
Using a functional component on your main routing page, you can have the title change on each route change with useEffect.
For example,
const Routes = () => {
useEffect(() => {
let title = history.location.pathname
document.title = title;
});
return (
<Switch>
<Route path='/a' />
<Route path='/b' />
<Route path='/c' />
</Switch>
);
}
I built a bit on Thierry Prosts solution and ended up with the following:
UPDATE January 2020: I've now updated my component to be in Typescript as well:
UPDATE August 2021: I've added my private route in TypeScript
import React, { FunctionComponent, useEffect } from 'react';
import { Route, RouteProps } from 'react-router-dom';
interface IPageProps extends RouteProps {
title: string;
}
const Page: FunctionComponent<IPageProps> = props => {
useEffect(() => {
document.title = "Website name | " + props.title;
});
const { title, ...rest } = props;
return <Route {...rest} />;
};
export default Page;
UPDATE: My Page.jsx component is now a functional component and with useEffect hook:
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
const Page = (props) => {
useEffect(() => {
document.title = "Website name | " + props.title;
});
const { title, ...rest } = props;
return <Route {...rest} />;
}
export default Page;
Below you can find my initial solution:
// Page.jsx
import React from 'react';
import { Route } from 'react-router-dom';
class Page extends Route {
componentDidMount() {
document.title = "Website name | " + this.props.title;
}
componentDidUpdate() {
document.title = "Website name | " + this.props.title;
}
render() {
const { title, ...rest } = this.props;
return <Route {...rest} />;
}
}
export default Page;
And my Router implementation looked like this:
// App.js / Index.js
<Router>
<App>
<Switch>
<Page path="/" component={Index} title="Index" />
<PrivateRoute path="/secure" component={SecurePage} title="Secure" />
</Switch>
</App>
</Router>
Private route setup:
// PrivateRoute
function PrivateRoute({ component: Component, ...rest }) {
return (
<Page
{...rest}
render={props =>
isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/",
state: { from: props.location }
}}
/>
)
}
/>
);
}
Private Route in TypeScript:
export const PrivateRoute = ({ Component, ...rest }: IRouteProps): JSX.Element => {
return (
<Page
{...rest}
render={(props) =>
userIsAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: Paths.login,
state: { from: props.location },
}}
/>
)
}
/>
);
};
This enabled me to have both public areas update with a new title and private areas also update.
With a little help from Helmet:
import React from 'react'
import Helmet from 'react-helmet'
import { Route, BrowserRouter, Switch } from 'react-router-dom'
function RouteWithTitle({ title, ...props }) {
return (
<>
<Helmet>
<title>{title}</title>
</Helmet>
<Route {...props} />
</>
)
}
export default function Routing() {
return (
<BrowserRouter>
<Switch>
<RouteWithTitle title="Hello world" exact={true} path="/" component={Home} />
</Switch>
</BrowserRouter>
)
}
Here is my solution which is almost the same as simply setting document.title but using useEffect
/**
* Update the document title with provided string
* #param titleOrFn can be a String or a function.
* #param deps? if provided, the title will be updated when one of these values changes
*/
function useTitle(titleOrFn, ...deps) {
useEffect(
() => {
document.title = isFunction(titleOrFn) ? titleOrFn() : titleOrFn;
},
[...deps]
);
}
This has the advantage to only rerender if your provided deps change.
Never rerender:
const Home = () => {
useTitle('Home');
return (
<div>
<h1>Home</h1>
<p>This is the Home Page</p>
</div>
);
}
Rerender only if my userId changes:
const UserProfile = ({ match }) => {
const userId = match.params.userId;
useTitle(() => `Profile of ${userId}`, [userId]);
return (
<div>
<h1>User page</h1>
<p>
This is the user page of user <span>{userId}</span>
</p>
</div>
);
};
// ... in route definitions
<Route path="/user/:userId" component={UserProfile} />
// ...
CodePen here but cannot update frame title
If you inspect the <head> of the frame you can see the change:
I am answering this because I feel you could go an extra step to avoid repetitions within your components and you could just get the title updated from one place (the router's module).
I usually declare my routes as an array but you could change your implementation depending on your style. so basically something like this ==>
import {useLocation} from "react-router-dom";
const allRoutes = [
{
path: "/talkers",
component: <Talkers />,
type: "welcome",
exact: true,
},
{
path: "/signup",
component: <SignupPage />,
type: "onboarding",
exact: true,
},
]
const appRouter = () => {
const theLocation = useLocation();
const currentLocation = theLocation.pathname.split("/")[1];
React.useEffect(() => {
document.title = `<Website Name> |
${currentLocation[0].toUpperCase()}${currentLocation.slice(1,)}`
}, [currentLocation])
return (
<Switch>
{allRoutes.map((route, index) =>
<Route key={route.key} path={route.path} exact={route.exact} />}
</Switch>
)
}
Another approach would be declaring the title already in each of the allRoutes object and having something like #Denis Skiba's solution here.
You also can go with the render method
const routes = [
{
path: "/main",
component: MainPage,
title: "Main Page",
exact: true
},
{
path: "/about",
component: AboutPage,
title: "About Page"
},
{
path: "/titlessPage",
component: TitlessPage
}
];
const Routes = props => {
return routes.map((route, idx) => {
const { path, exact, component, title } = route;
return (
<Route
path={path}
exact={exact}
render={() => {
document.title = title ? title : "Unknown title";
console.log(document.title);
return route.component;
}}
/>
);
});
};
the example at codesandbox (Open result in a new window for see title)
Please use react-helmet. I wanted to give the Typescript example:
import { Helmet } from 'react-helmet';
const Component1Title = 'All possible elements of the <head> can be changed using Helmet!';
const Component1Description = 'No only title, description etc. too!';
class Component1 extends React.Component<Component1Props, Component1State> {
render () {
return (
<>
<Helmet>
<title>{ Component1Title }</title>
<meta name="description" content={Component1Description} />
</Helmet>
...
</>
)
}
}
Learn more: https://github.com/nfl/react-helmet#readme
Dan Abramov (creator of Redux and current member of the React team) created a component for setting the title which works with new versions of React Router also.
It's super easy to use and you can read about it here:
https://github.com/gaearon/react-document-title
For instance:
<DocumentTitle title='My Web App'>