I am trying to use the react context api. I am experiencing an issue though where the context/state value is not updating. I have no idea why this is happening and have looked at numerous threads but have found that nothing works for me.
Here is the code:
curtain-context.js
For creating the contexts and exporting them:
const CurtainContext = createContext({
curtainVisible: false,
setCurtainVisible: (value) => {}
});
export function CurtainContextProvider(props) {
const [curtainVisible, setCurtainVisible] = useState();
function setCurtainVisibleHandler(value) {
setCurtainVisible(value);
console.log(value);
}
const context = {
curtainVisible: curtainVisible,
setCurtainVisible: setCurtainVisibleHandler
};
return (
<CurtainContext.Provider value={context}>
{props.children}
</CurtainContext.Provider>
);
}
export default CurtainContext;
App.js
The main application code which is surrounded by the context provider:
<Layout>
<CurtainContextProvider>
<Routes>
<Route element={<HomePage/>} path='/' exact/>
<Route element={<HomePage/>} path='/home' exact/>
<Route element={<ServicesPage/>} path='/services' exact/>
<Route element={<ProductsPage/>} path='/products' exact/>
<Route element={<ContactPage/>} path='/contact' exact/>
<Route element={<LoginPage/>} path='/login' exact/>
</Routes>
</CurtainContextProvider>
</Layout>
MainNavigation.js
The place where I want to use the context value to render something if curtainVisible is true:
import { NavLink } from 'react-router-dom';
import classes from './MainNavigation.module.css';
import React, {useContext, useState} from "react";
import { useLocation } from "react-router";
import MobileCurtain from "../ui/MobileCurtain";
import CurtainContext from "../../store/curtain-context";
function MainNavigation() {
var curtainContext = useContext(CurtainContext);
const { pathname } = useLocation();
const activeClass = ({isActive}) => (isActive ? classes.active : classes.inactive);
const activeUserClass = ({paths = ['/login', '/settings']}) => (paths.includes(pathname) ? classes.active : classes.inactive);
function handleBarsClicked() {
curtainContext.setCurtainVisible(true);
}
return (
<div className={classes.menu}>
<ul>
<li className={classes.textLinkBars}><button className={classes.iconButton} onClick={handleBarsClicked}><FontAwesomeIcon icon={faBars} className={classes.bars}/></button></li>
{ curtainContext.curtainVisible ? <MobileCurtain/> : null}
<li className={classes.textLink}><NavLink to="/" className={activeClass}>Home</NavLink></li>
<li className={classes.textLink}><NavLink to="/services" className={activeClass}>Services</NavLink></li>
<li className={classes.textLink}><NavLink to="/products" className={activeClass}>Products</NavLink></li>
<li className={classes.textLink}><NavLink to="/contact" className={activeClass}>Contact</NavLink></li>
</div>
</ul>
</div>
);
}
export default MainNavigation;
Only components that are descendants of the Provider can use context value.
In your example, MainNavigation isn't a descendant of CurtainContextProvider hence the issue.
You set your initial value to
{
curtainVisible: false,
setCurtainVisible: (value) => {}
}
which didn't helped, because this (value) => {} was run instead of setCurtainVisibleHandler.
I would suggest using undefined as an initial value of context
Also, hooks like this can help prevent the issue like yours:
const useCurtainContext = () => {
const context = useContext(CurtainContext);
if (!context) {
throw new Error('`useCurtainContext` have to be used inside `CurtainContextProvider`')
}
return context
}
I have a React app which doens't have a route to / setup, so I placed a redirect inside the Switchcomponent, so whenever the user tries to access home they are redirected to the UserHome component.
The Switch set-up is as follows:
const AppRoutes = () => (
<Switch>
<Redirect
exact
from="/"
to={ROUTES.CITIZEN}
/>
<Route
exact
path="/login"
component={Login}
/>
<AuthenticatedRouteRedirect
path={ROUTES.CITIZEN}
component={UserHome}
/>
<AuthenticatedRouteRedirect
path={ROUTES.ADMIN_REPORT_LIST}
component={reportList}
/>
<Route
path="/404"
component={ErrorView}
/>
<Route
component={ErrorView}
/>
</Switch>
And AuthenticatedRouteRedirect as follows:
const AuthenticatedRouteRedirect = ({
component: Component, path = '', exact = false, ...rest
}) => {
const { user } = useAuth();
return (
<Route
path={path}
exact={exact}
render={() => (user
? <Component {...rest} />
: <Redirect to="/login" />)}
/>
);
};
export default AuthenticatedRouteRedirect;
And my UserHome component:
const Dashboard = () => (
<>
<Navbar />
<Container>
<ActionMenu />
</Container>
</>
);
where ActionMenu component uses a custom hook called useReportsLocations, which its implementation is:
import React from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { getMapPolygons, mexicoMap } from 'core/map-locations';
const useReportsLocations = (selectedGroup) => {
const { push } = useHistory();
const { state, municipality } = useParams();
const locationData = React.useMemo(() => {
const currentSelection = {
country: mexicoMap,
state,
municipality,
group: selectedGroup,
};
return getMapPolygons(currentSelection);
}, [municipality, selectedGroup, state]);
React.useEffect(() => {
if (!locationData?.stateData
|| !Object.keys(locationData?.stateData?.municipality
|| {}).includes(municipality)) {
push('/404');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [municipality]);
return {
locationData,
state,
municipality,
};
};
export default useReportsLocations;
Here's a little demo on CodeSandBox
But when trying to access /, this error message is displayed with the next stacktrace, sorry if it's too large:
What is the cause for this to happen? And how to fix it?
I'm having a bad time trying to figure out whatever the cause is, but simply I can't.
Thank you for your answer and replies.
Pd
I finally figured out what's happening.
After I read the docs, as Redirect leads me to a route where some params are required, but I don't provide any ones to to (as seen in the ROUTES file), it has only the placeholders of {ROUTES.CITIZEN} route, and path-to-regexp#^1.7.0 will complaint that it has been passed nothing and coudn't not resolve to anything.
This behaviour is expected.
How do I set up conditional routes with exclusive Components in React?
I want the upload route to be exclusive to when authenticated, which is set in ComponentdDidMount.
render = () => {
let routes = (
<Switch>
<Route path = "/" exact render = {() => <Home />
<Route path = "/video" render = {() => <Video />} />
<Route path = "/upload" exact render = {() => <Upload />} />
<Redirect to = "/foo"/>
</Switch>
)
if(this.props.isAuthenticated){
routes = (
<Switch>
<Route path = "/" exact render = {() => <Dashboard />} />
<Route path = "/upload`" render = {() => <Upload />} />
<Route path = "/video" render = {() => <Video />} />
<Route path = "/foo" render = {() => <h1>Foo</h1>} />
<Redirect to = "/bar" />
</Switch>
)
}
return (
<div className="App">
<Layout>
{routes}
</Layout>
</div>
)
}
Right it's using the first set of Route components to check the route, and if the route doesn't match I get redirected to '/foo' which then renders the h1. If I try to access 'upload', I believe it gets rendered for a split second, and then I end up with infinite redirects to '/bar'. '/video' does render the video component. Can someone provide some information as to what may be going wrong and how I can make sure only one set of Routes is being used?
Maybe just need to handle authentication on private routes, you can get more info here
I had the same question a few months ago, and what I did is to create a wrapper to check if user has the necessary rights to see that item.
render: () => AuthorizeRoute(
Amortizations, // Component to load
'anyvalidRole4ex' // {string} constant
),
If you are using redux, could be something like this:
import React from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import NotAllowed from './NotAllowed';
export default function HasPermissionWrapper(
WrappedComponent,
requiredRoles,
FallbackComponent
) {
class HasPermission extends React.PureComponent {
static propTypes = {
userRoles: PropTypes.object.isRequired,
};
render() {
const userRoles = this.props.userRoles.toJS();
const hasPermission = userRoles
.map(({roleId}) => requiredRoles.includes(roleId))
.some((checks) => !!checks);
if (!hasPermission) {
if (FallbackComponent) {
return <FallbackComponent />;
}
return <NotAllowed userRoles={userRoles} />;
}
return <WrappedComponent {...this.props} />;
}
}
const mapStateToProps = ({auth}) => ({
userRoles: auth.getIn(['user', 'roles']),
});
return connect(mapStateToProps)(HasPermission);
}
I'm looking for a way to do some route protection with react-router-4. Looking at an example in the documentation, they create a Component which is rendered like this:
<PrivateRoute path="/protected" component={Protected} />
and the privateRoute component:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
fakeAuth.isAuthenticated ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}}/>
)
)}/>
)
I don't really understand why I should want to pass the "Protected" component as a property and then have to take care of spreading all the ...props and ...rest.
Before I was reading this doc (and other example), I created the following code, which just nest the routes in another component which takes care of the authentication part.
Because my example (which seems to work perfectly well), looks way more simplistic, I must be missing something.
Are there any downsides on this approach?
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import Nav from './Nav';
// dummy
const Auth = {
isAuthenticated: () => { return true; }
}
const Home = () => <h1>Home</h1>
const SignIn = () => <h1>SignIn</h1>
const About = () => <h1>About</h1>
class PrivateOne extends Component {
render() {
console.log(this.props);
return <h1>Private</h1>
}
}
const PrivateTwo = () => <h1>PrivateTwo</h1>
const PrivateThree = () => <h1>PrivateThree</h1>
const NotFound = () => <h1>404</h1>
const Private = ({isAuthenticated, children}) => {
return(
isAuthenticated ? (
<div>
<h1>Private</h1>
{children}
</div>
) : (
<Redirect to={{
pathname: '/sign_in',
}}/>
)
)
}
const App = () =>
<div>
<Router>
<div>
<Nav />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/sign_in" component={SignIn} />
<Private isAuthenticated={Auth.isAuthenticated()}> {/* or some state later on */}
<Route path="/private1" component={PrivateOne} />
<Route path="/private2" component={PrivateTwo} />
<Route path="/private3" component={PrivateThree} />
</Private>
<Route component={NotFound} />
</Switch>
</div>
</Router>
</div>
export default App;
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'>