有没有办法用 React-Router v4 + 修改页面标题?

我正在寻找一种方法来修改页面标题时,反应路由器 v4 + 更改位置。我过去常常在 Redux 中侦听位置更改操作,并根据 metaData对象检查该路由。

当使用 React-Router v4 + 时,没有固定路由列表。事实上,站点周围的各个组件可以使用具有相同路径字符串的 Route。也就是说我以前用的方法不管用了。

有没有一种方法,我可以更新页面标题通过调用动作时,某些主要路线被改变或有一个更好的方法,更新网站的元数据?

83866 次浏览

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.

<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>
)}
/>

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]);
};

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: screenshot

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>
);
}

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'>

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.