How to change react-router routes? - javascript

I'm trying to change routes depending on logged in state:
renderRouter() {
if (loggedIn) {
return (
<Router>
<Route path="/" component={Dashboard} />
</Router>
);
}
return (
<Router>
<Route path="/" component={Login} />
</Router>
);
}
But when state changes I'm receiving a warning: Warning: [react-router] You cannot change <Router routes>; it will be ignored
Is it possible to reinitialize react-router with new routes?
I know that I could use onEnter to ensure that user has access to this page, but I need to have different components in one route according to logged in state and don't want to move such logic inside of components.

First of all you create two Router, I don't think you should do this.
Try wrapping your Routes in a Switch component which is in only one Router, then use the render props of your "main" Route, which will redirect you if the condition is true, use exact props to be sure this Route will match by default, notice that your "/dashboard" Route is above the other, so the Switch can match it.
The result should be like this :
<Router>
<Switch>
<Route path="/dashboard" component={Dashboard} />
<Route
path="/"
exact
render={() => {
if (loggedIn) {
return <Redirect to="/dashboard" />;
}
return <Login />;
}}
/>
</Switch>
</Router>
Don't forget to import the components.
Hope it helped.

I've tried few times to gain desired behavior and after all decided to change an approach of secure endpoints management. Components on my endpoints are very simple scenes which are just compose layer and some scene modules. So I've created a scene wrapper:
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Login from '../scenes/Login';
function Scene({ children, fallback, secure, auth }) {
if (secure === auth) {
return children;
}
return React.createElement(fallback);
}
Scene.propTypes = {
children: PropTypes.node.isRequired,
auth: PropTypes.bool,
fallback: PropTypes.func,
secure: PropTypes.bool,
};
Scene.defaultProps = {
auth: false,
fallback: Login,
secure: false,
};
const mapStateToProps = ({ auth }) => ({ auth });
export default connect(mapStateToProps)(Scene);
And then in Dashboard scene:
import React from 'react';
import Scene from '../modules/Scene';
import Layout from '../components/Layout';
export default function Dashboard() {
return (
<Scene secure>
<Layout>
<Module1 />
<Module2 />
</Layout>
</Scene>
);
}

Related

Upgrading from react router v5 to v6

Following this tutorial series to try to build a simple React, Electron, and firebase app.
This project mirrors this demo project. I'm getting a lot of compile errors, mostly outdated content and dependencies, but managed to fix most of them up. The main thing I'm struggling with now is upgrading some code from react-router v5 to v6, specifically in app.js
import React, { useState, useEffect } from "react";
import { Router, Routes, Route, Navigate } from "react-router-dom";
import AddMoviePage from "../pages/add-movie-page";
import EditMoviePage from "../pages/edit-movie-page";
import AccountPage from "../pages/account-page";
import MoviesPage from "../pages/movies-page";
import NotFoundPage from "../pages/not-found-page";
import { auth } from "../data/firebase";
import Nav from "./nav";
import { createMemoryHistory } from "history";
function AuthenticatedRoute(props) {
const { isAuthenticated, children, ...routeProps } = props;
return <Route {...routeProps}>{isAuthenticated ? children : <Navigate to="/account" />}</Route>;
}
function App() {
const [user, setUser] = useState(null);
const isAuthenticated = user !== null;
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((currentUser) => {
setUser(currentUser);
});
return unsubscribe;
}, []);
const history = createMemoryHistory();
console.log(history);
return (
<Router history={history}>
<Nav user={user} />
<Routes>
<Route path="/account">
<AccountPage user={user} />
</Route>
<AuthenticatedRoute path="/" exact isAuthenticated={isAuthenticated}>
<MoviesPage user={user} />
</AuthenticatedRoute>
<AuthenticatedRoute path="/add" isAuthenticated={isAuthenticated}>
<AddMoviePage user={user} />
</AuthenticatedRoute>
<AuthenticatedRoute path="/edit/:id" isAuthenticated={isAuthenticated}>
<EditMoviePage user={user} />
</AuthenticatedRoute>
<Route path="*">
<NotFoundPage />
</Route>
</Routes>
</Router>
);
}
export default App;
I'm getting the following error and can't really figure out what's going on:
Uncaught TypeError: Cannot read properties of undefined (reading 'pathname')
The above error occurred in the <Router> component.
Issues
The main issue here is that you are importing and using the low-level Router component instead of one of the high-level routers (i.e. BrowserRouter, MemoryRouter, HashRouter, etc). The Router component has a couple required props and history isn't one of them.
Router Interface:
declare function Router(
props: RouterProps
): React.ReactElement | null;
interface RouterProps {
basename?: string;
children?: React.ReactNode;
location: Partial<Location> | string; // <-- required!
navigationType?: NavigationType;
navigator: Navigator; // <-- required!
static?: boolean;
}
The high-level routers all instantiate/manage a history reference internally and pass the required props and render the base Router.
Additional issues found in the code:
Another issue is that in react-router-dom#6 custom route components are no longer valid. Only Route components can be rendered by the Routes component. You'll instead convert your older v5 custom route components, a.k.a. AuthenticatedRoute, either into Wrapper components that render the children prop, or as the preferred method a Layout Route.
A final related issue is that Route components and only be rendered by the Routes component or other Route components in the case of building nested routes. In other words, the only valid children components of a Route component is another Route component. The routed content you want to be rendered on a route is passed to the Route component's element prop.
Solution
Convert AuthenticatedRoute to a layout route.
import { Navigate, Outlet } from 'react-router-dom';
function AuthenticatedRoute({ isAuthenticated }) {
if (isAuthenticated === undefined) {
// Don't render the protected content or redirect until we confirm
// authentication status.
return null; // or loading indicator/spinner/etc
}
return isAuthenticated ? <Outlet /> : <Navigate to="/account" replace />;
}
It seems you are wanting to really use a MemoryRouter since you are instantiating your own MemoryHistory object. Import and render the MemoryRouter directly. Move the route "children" onto their respective route's element prop.
Example:
...
import {
MemoryRouter as Router, // <-- import high-level router
Routes,
Route,
Navigate,
Outlet
} from "react-router-dom";
...
function AuthenticatedRoute({ isAuthenticated }) {
if (isAuthenticated === undefined) {
return null; // or loading indicator/spinner/etc
}
return isAuthenticated ? <Outlet /> : <Navigate to="/account" replace />;
}
function App() {
const [user, setUser] = useState(); // <-- initially not auth'd or unauth'd
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((currentUser) => {
setUser(currentUser); // <-- sets to user object or null
});
return unsubscribe;
}, []);
return (
<Router> // <-- Now really a MemoryRouter
<Nav user={user} />
<Routes>
<Route path="/account" element={<AccountPage user={user} />} />
<Route element={<AuthenticatedRoute isAuthenticated={user} />}>
<Route path="/" element={<MoviesPage user={user} />} />
<Route path="/add" element={<AddMoviePage user={user} />} />
<Route path="/edit/:id" element={<EditMoviePage user={user} />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Router>
);
}
export default App;

Redirecting to pages depending on conditionals in ReactJS

Given is an application with 3 pages:
"Mainpage"
"PrivatPage"
"UserManagementPage"
After successful login the user is redirected to a "PrivatPage". On the "PrivatPage" the user has the possibility to return to the "MainPage" or to go to the "UserManagementPage". The part with the redirecting from the "MainPage" to the "PrivatPage" works.
The code looks like this:
import PublicPage from "./components/PublicPage";
import PrivatePage from "./components/PrivatePage";
import UserManagementPage from "./components/UserManagementPage";
import React, { useState, Component } from "react";
import { connect } from "react-redux";
const mapStateToProps = state => {
return state;
};
class App extends Component {
render() {
const accessToken = this.props.accessToken;
console.log(accessToken);
let workspace;
if (accessToken) {
workspace = <PrivatePage />;
} else {
workspace = <PublicPage />;
}
return <div className="App">{workspace}</div>;
}
}
export default connect(mapStateToProps)(App);
But how do I use the conditionals to get to the "UserManagementPage" ?
if you consider functional components, you could use BrowserRouter as follows with react-router-dom.
If you need to handle authentication, you can f.e. build a custom <PrivateRoute /> component and use this on your protected routes instead of <Route />. I always keep these routes in a separate file and import them in App.js.
Here for demo purposes routes in App.js:
import { BrowserRouter as Router } from "react-router-dom";
// import your page components
// and add everything else you want to add to your component
const App = () => {
return (
<>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/private" element={<PrivatePage />} />
<Route path="/public" element={<PublicPage />} />
<Route path="/user" element={<UserManagementPage />} />
</Routes>
</Router>
</>
);
};
export default App;
Adding on to private blocks answer you would then in your components use the
<Redirect to='/your-route' />
You would then create a boolean state variable and once it return true you could redirect immediatley like this (where you are rendering jsx):
render() {
{booleanState && <Redirect to='/your-route' />}
}

Unmounted component with react-router

I have a problem with my react router when I want to pass from a router to another.
I have a Connexion files :
import React from "react";
import {
Switch,
BrowserRouter as Router,
Route,
Redirect,
} from "react-router-dom";
import SignIn from "./Containers/Login/Login";
import Player from "./Player";
const Routes = () => {
return (
<Router>
<Switch>
<Route exact path="/" component={SignIn} />
<Route path="/app/player/" component={Player} />
<Route path="/*">
<Redirect to="/" />
</Route>
</Switch>
</Router>
);
};
export default Routes;
When I connect from "SignIn", I want to go in the next route "Player" :
import React from "react";
import {
Switch,
BrowserRouter as Router,
Route,
useRouteMatch,
} from "react-router-dom";
import Navigation from "./Navigation/PlayerNavigation";
import FillCollect from "./Containers/Collect/Collect";
import Dashboard from "./Containers/Dashboard/Dashboard";
const Player = () => {
const { path } = useRouteMatch();
console.log(path);
return (
<Router>
<Navigation />
<Switch>
<Route path={`${path}/collect/:id`} component={FillCollect} />
</Switch>
</Router>
);
};
export default Player;
(My Navigation component is my sidebar and topbar)
But when I'm connect my Navigation is displayed but not my Fill Collect ! And i have this warning :
Warning: Can't perform a React state update on a unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect clean function.
In SignIn (created by Context.Consumer)
I tried to display a minimalist component with just div but it's the same
There are a couple of problems in this
Firstly you are using a nested Router component too, which then restricts the inner Routes from listening to the router Router component
Secondly, your matchPath in parent is "/app/player/" which if you combine with the nested Route in child like ${path}/collect/:id. It becomes "/app/player//collect/:id" which is not what you indent
The working solution will be as follows
const Routes = () => {
return (
<Router>
<Switch>
<Route exact path="/" component={SignIn} />
<Route path="/app/player" component={Player} />
<Route path="/*">
<Redirect to="/" />
</Route>
</Switch>
</Router>
);
};
export default Routes;
const Player = () => {
const { path } = useRouteMatch();
console.log(path);
return (
<React.Fragment>
<Navigation />
<Switch>
<Route path={`${path}/collect/:id`} component={FillCollect} />
</Switch>
</React.Fragment>
);
};
export default Player;

React private Route infinite render - Getting error Error: Maximum update depth exceeded

I am trying to render my components inside private routes if the user is authed. But I keep getting the error.
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
To see if a user is authed I get the token from the localStorage.
The commented line const authed = true was for testing purpose, and it works.
My Routes component looks like this:
import React from 'react';
import { Switch, Redirect, Route } from 'react-router-dom';
import { RouteWithLayout } from './components';
import { Main as MainLayout, Minimal as MinimalLayout} from './layouts';
import {
Dashboard as DashboardView,
ProductList as ProductListView,
UserList as UserListView,
Typography as TypographyView,
Icons as IconsView,
Account as AccountView,
Settings as SettingsView,
NotFound as NotFoundView,
Login as LoginView,
} from './views';
const Routes = () => {
const authed = !!localStorage.getItem('token');
//const authed = true;
return (
<Switch>
<RouteWithLayout authed={authed} component={LoginView} exact layout={MinimalLayout} path="/login" />
<Redirect exact from="/" to="/login" />
{/*authed={this.state.authed}*/}
<RouteWithLayout authed={authed} component={DashboardView} exact layout={MainLayout} path="/dashboard" />{' '}
<RouteWithLayout authed={authed} component={UserListView} exact layout={MainLayout} path="/users" />
<RouteWithLayout authed={authed} component={ProductListView} exact layout={MainLayout} path="/products" />
<RouteWithLayout authed={authed} component={TypographyView} exact layout={MainLayout} path="/typography" />
<RouteWithLayout authed={authed} component={IconsView} exact layout={MainLayout} path="/icons" />
<RouteWithLayout authed={authed} component={AccountView} exact layout={MainLayout} path="/account" />
<RouteWithLayout authed={authed} component={SettingsView} exact layout={MainLayout} path="/settings" />
<RouteWithLayout authed={authed} component={NotFoundView} exact layout={MinimalLayout} path="/not-found" />
<Redirect to="/not-found" />
</Switch>
);
};
export default Routes;
And my RouteWithLayout looks like:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
const RouteWithLayout = props => {
const { authed, layout: Layout, component: Component, ...rest } = props;
return (
<Route
{...rest}
render={matchProps =>
authed === true ? (
<Layout>
<Component {...matchProps} />
</Layout>
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
);
};
RouteWithLayout.propTypes = {
authed: PropTypes.any,
component: PropTypes.any.isRequired,
layout: PropTypes.any.isRequired,
path: PropTypes.string,
props: PropTypes.any.isRequired,
};
export default RouteWithLayout;
It looks like if auth === false, you will infinitely redirect because the /login route uses RouteWithLayout.
Given auth === false:
Navigate to /login
RouteWithLayout logic is used
If auth isn't true, redirect to /login
These steps just get repeated over and over, so you need a special case for the login route — lots of different ways to implement that.
Side note: you have a stray {' '} after your dashboard route.
Another side note: You're probably still working through this part, but worth mentioning that this is VERY insecure: const authed = !!localStorage.getItem('token'); That code means that any non falsy value will be accepted as a valid token, meaning I could manually just add a dummy token to localStorage and authenticate.

React Router Duplicate routes on Back Button

I have a react app. It is working fine. It uses redux,react-router 3. The routes work fine, but when I press the back button, they route gets duplicated. For example from localhost:3000/admin/main which I am currently, when I go back, it goes to localhost:3000/admin/admin/main, which return not found.
Here is my routes code:
export default (
<Route path="/" component={App}>
<Route path="home" component={requireNoAuthentication(HomeContainer)} />
<Route path="login" component={requireNoAuthentication(LoginView)} />
<Route exact path="admin/user" component={requireAuthentication(UserView)} />
<Route exact path="admin/main" component={requireAuthentication(UsersListView)} />
<Route path="secure" component={requireAuthentication(CustomerView)} />
<Route exact path="*" component={DetermineAuth(NotFound)} />
</Route>
);
I also get a console error: Adjacent JSX elements must be wrapped in an enclosing tag. If anyone can help it would be great thanks!!
Your HOC wrappers (requireNoAuthentication and requireAuthentication) and using exact (I think this might a react-router v4 only feature?) might be messing with your route history. Try restructuring your routes so that all of them fall under App -- some of the routes fall under RequireAuth, while the rest are public.
As a side note: you can avoid using React.cloneElement with passed down class methods and state by using Redux instead.
routes/index.js
import React from "react";
import { browserHistory, IndexRoute, Router, Route } from "react-router";
import App from "../components/App";
import Home from "../components/Home";
import Info from "../components/Info";
import ShowPlayerRoster from "../components/ShowPlayerRoster";
import ShowPlayerStats from "../components/ShowPlayerStats";
import Schedule from "../components/Schedule";
import Sponsors from "../components/Sponsors";
import RequireAuth from "../components/RequireAuth";
export default () => (
<Router history={browserHistory}>
<Route path="/" component={App}>
<Route component={RequireAuth}>
<IndexRoute component={Home} />
<Route path="roster" component={ShowPlayerRoster} />
<Route path="roster/:id" component={ShowPlayerStats} />
<Route path="schedule" component={Schedule} />
</Route>
<Route path="info" component={Info} />
<Route path="sponsors" component={Sponsors} />
</Route>
</Router>
);
index.js
import React from "react";
import { render } from "react-dom";
import App from "../routes";
import "uikit/dist/css/uikit.min.css";
render(<App />, document.getElementById("root"));
components/App.js
import React, { Component, Fragment } from "react";
import { browserHistory } from "react-router";
import Header from "./Header";
export default class App extends Component {
state = {
isAuthenticated: false
};
isAuthed = () => this.setState({ isAuthenticated: true });
unAuth = () =>
this.setState({ isAuthenticated: false }, () => browserHistory.push("/"));
render = () => (
<Fragment>
<Header
isAuthenticated={this.state.isAuthenticated}
unAuth={this.unAuth}
/>
{React.cloneElement(this.props.children, {
isAuthenticated: this.state.isAuthenticated,
isAuthed: this.isAuthed
})}
</Fragment>
);
}
components/RequireAuth.js
import React, { Fragment } from "react";
import Login from "./Login";
export default ({ children, isAuthenticated, isAuthed }) =>
!isAuthenticated ? (
<Login isAuthed={isAuthed} />
) : (
<Fragment>{children}</Fragment>
);

Categories