dynamic basename with BrowserRouter in react-router-dom - javascript

Please I have an issue building a multi-tenant SaaS solution. For every tenant, I want them to use a subdomain, so i can get the subdomain from the url, make a call to a REST api that returns data about that tenant.
For example,
the admin (another app entirely - admin app) creates a tenant with domain name: tenant1.
In the tenant application on my local system, I was able to go to tenant1.localhost:3000. I get the url, and get the domain name. I then make a call with the domain to get the theme of tenant (this is stored in localStorage).
Unfortunately, we deploy on k8 in my company and so I couldn't mimic this behavior. So i have been advised by the devOps team to use subdomain in the context, thereby having localhost:3000/tenant1. Remember the tenant is dynamic, so i tried this:
<BrowserRouter basename={"/:tenant"}>
<Switch>
<Route exact path="/login" name="Login" component={Login} />
<Route exact path="/set-password/:token" name="Set Password" component={SetPassword} />
<PrivateRoute path="/" name="Default Layout" component={DefaultLayout} />
</Switch>
</BrowserRouter>
The solution above however makes my url to localhost:3000/:tenant/login
Please how can i use dynamic basename in the router, so it can accept:
localhost:3000/tenant1
localhost:3000/tenant3
localhost:3000/tenant2 etc.
It can allow any, my app handles wrong domain inputted

I finally used dynamic tenant with the following code
class App extends Component {
state = {
domain: ""
}
componentWillMount () {
const { domain } = this.state;
const parsedData = window.location.pathname.split("/");
let domain = parsedData[1];
this.setState({ domain: domain })
this.props.onGetTenant(domain);
}
render () {
const { domain } = this.state;
return () {
<BrowserRouter basename={"/"+domain}>
<Switch>
<Route exact path="/login" name="Login" component={Login} />
<Route exact path="/set-password/:token" name="Set Password" component={SetPassword} />
<PrivateRoute domain={domain} path="/" name="Default Layout" component={DefaultLayout} />
</Switch>
</BrowserRouter>
}
const mapStateToProps = state => {
const { tenant} = state;
return { tenant};
};
const mapDispatchToProps = (dispatch) => {
return {
onGetTenant: bindActionCreators( tenantActions.get, dispatch)
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App)

This worked for me using react >16 and react-router-dom v5
export const App = () => {
return (
<BrowserRouter>
<Switch>
<Route path="/:tenantId?" component={LayoutRoot} />
</Switch>
</BrowserRouter>
);
};
export const LayoutRoot = () => {
var { tenantId } = useParams();
//TODO: add some validation here and inform user if tenant is invalid
return (
<BrowserRouter basename={tenantId}>
<Switch>
<Route path="/login" component={LoginComponent} />
<Route path="/dashboard" component={DashboardComponent} />
</Switch>
</BrowserRouter>
);
};

You can render updates to your router's basename by using the key property. Any changes to the key value will cause the component to re-render.
Here's a code sandbox to demo:
https://codesandbox.io/s/react-router-dom-dynamic-basename-forked-hnkk0?file=/index.js
You can hover or inspect the links in the sandbox to verify that their href values are correctly updating after changing the basename. You can also see that the hrefs won't update if you remove the key property from Router.
import React, { useState } from "react";
import { render } from "react-dom";
import { BrowserRouter, Link } from "react-router-dom";
const Root = () => {
const [count, setCount] = useState(1);
const basename = `basename-${count}`;
return (
<BrowserRouter basename={basename} key={basename}>
<Link to="/link1">Link 1</Link>
<br />
<Link to="/link2">Link 2</Link>
<br />
Current basename: {basename}
<br />
<button onClick={() => setCount(count + 1)}>change basename</button>
</BrowserRouter>
);
};
render(<Root />, document.getElementById("root"));

Here's a codesandbox and the utility I wrote:
https://codesandbox.io/s/react-router-dom-dynamic-basename-xq9tj?file=/index.js
import urlJoin from 'url-join';
// it's important to have an identifier in their app
export const APP_ROOT_URL = '/my-app';
export const getBaseUrlPath = () => {
const currentPath = document.location.pathname || APP_ROOT_URL;
const startOfAppBase = currentPath.indexOf(APP_ROOT_URL);
let base = currentPath;
if (startOfAppBase !== -1) {
base = currentPath.substr(0, startOfAppBase);
}
base = urlJoin(base, APP_ROOT_URL);
return base;
};

Related

Nested routing using protected routes is not working properly

earlier I posted a similar question: Method for checking admin is redirecting me to main page when trying to login
I tried to implement the protected route, inside a protectRoute.tsx:
import { Navigate, Outlet } from "react-router";
import { RootStateOrAny, useSelector } from "react-redux";
interface ProtectRouteProps {}
export default function ProtectRoute(props: ProtectRouteProps) {
const userSignin = useSelector((state: RootStateOrAny) => state.userSignin);
const { userInfo } = userSignin;
return userInfo?.user?.isAdmin ? <Outlet /> : <Navigate to='/' />;
}
I don't really know what ProtectRouteProps is and what I should put in it. Also in the routing part I did like he told me:
<Route path='/createItem' element={<ProtectRoute />} />
<Route path='/createItem' element={<CreateItem />} />
The problem now is that can't access CreateItem because is going on the ProtectRoute page that is an empty one. What should i do?
I don't really know what ProtectRouteProps is and what I should put in it.
There are no props. This is clear by the usage:
<Route path='/createItem' element={<ProtectRoute />} />
No props are passed to ProtectRoute. You can drop the props object:
import { Navigate, Outlet } from "react-router";
import { RootStateOrAny, useSelector } from "react-redux";
export default function ProtectRoute() {
const userSignin = useSelector((state: RootStateOrAny) => state.userSignin);
const { userInfo } = userSignin;
return userInfo?.user?.isAdmin ? <Outlet /> : <Navigate to='/' replace />;
}
The problem now is that can't access CreateItem because is going on
the ProtectRoute page that is an empty one. What should i do?
"Auth" routes are what are called layout routes. They apply some logic, perhaps some styled layout CSS, and render an Outlet for nested Route components to be rendered into. The nested Route components use the path prop for route matching.
Example:
<Route element={<ProtectRoute />}>
<Route path='/createItem' element={<CreateItem />} />
... other protected routes ...
</Route>
<Route exact path='/Login' element={<Login name="Login Page"></Login>}></Route>
<Route element={<Protected/>}>
<Route exact path='/' element={<Home/> }></Route>
<Route exact path='/Content' element={<Content />}></Route>
</Route>
<Route path='*' element={<Login/>} ></Route>
</Routes>
Create Protected.js
import { Navigate, Outlet } from 'react-router-dom';
const useAuth = ()=>{
if(localStorage.getItem("isLogged")){
const user = {loggedIN :true};
return user && user.loggedIN
}else{
const user = {loggedIN :false};
return user && user.loggedIN
}
}
const Protected = () => {
const isAuth = useAuth();
return isAuth ? <Outlet/>:<Navigate to={"/login"}/>
}
export default Protected

Prevent user from going back on browser if already logged in (react)

I would like to refresh the current page (home) when the user tries to go back via browser, after logged in.
What's the best way to solve this? Any suggestions?
I was trying to do something like this inside index.tsx:
if (id) {
const rollback = history.goBack();
if (rollback) {
history.push('/');
}
}
Obs: In this case, '/' is my home page, and i can't apply the logic above because "An expression of type 'void' cannot be tested for truthiness".
Sorry for anything i'm still new at react and trying to learn.
Don't know if i could do something inside my router, here it is anyway:
import React, { Suspense, lazy } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import history from '../utils/history';
import LoadingPage from '../components/organisms/LoadingPage';
const DownloadExams = lazy(() => import('../pages/private/DownloadExams'));
const Home = lazy(() => import('../pages/private/Home'));
const ProfileSelector = lazy(() => import('../pages/private/ProfileSelector'));
const AppRoutes = () => {
return (
<Router history={history}>
<Suspense fallback={<LoadingPage />}>
<Switch>
<Route exact path={'/'} component={Home} />
<Route exact path={'/baixar-exames'} component={DownloadExams} />
<Route exact path={'/profile'} component={ProfileSelector} />
</Switch>
</Suspense>
</Router>
);
};
export default AppRoutes;
Any suggestions?
Thanks!
User logIn time you can store a token or flag and store it in localStorage. After that, you can check if the user login so redirects to the page. You can also create some HOC for the same.
Example :
import React from "react";
import { Route, Redirect } from "react-router-dom";
export const ProtectedRoute = ({ component: Component, ...rest }) => {
const isLoggedIn = localStorage.getItem("token");
return (
<Route
{...rest}
render={(props) => {
if (isLoggedIn) {
return <Component {...props} />;
} else {
return (
<Redirect
to={{
pathname: "/",
state: {
from: props.location,
},
}}
/>
);
}
}}
/>
);
};
Example Usage :
<ProtectedRoute path="/home" exact component={Home} />
This will redirect the user to /home after login.

React Router - Cannot read property 'history' of undefined

I am building a styleguide app. I have two dropdown components where a user can choose both a brand and a component - the app will then display the chosen component branded according to the selected brand. I want both of these options to be included in the URL.
The two dropdown's that are programatically changing the route. I am getting the error TypeError: Cannot read property 'history' of undefined whenever the user interacts with either dropdown.
I have a component being rendered by a route :
<Route path="/:brand/:component"
render={props => <DisplayComponent {...props} />} />
That component has two event handlers for two dropdown component that let the user select the root:
handleComponentPick(event: any) {
const component = event.target.value;
this.props.history.push(`/${this.props.match.params.brand}/${component}`);
}
handleBrandChange = (event: any) => {
if (event.target instanceof HTMLElement) {
const brand = event.target.value;
this.props.history.push(`/${brand}/${this.props.match.params.component}`);
}
};
render = () => {
return (
<div className={"constrain-width-wide center "}>
<ThemePicker
component={this.props.match.params.component}
brand={this.props.match.params.brand}
handleBrandChange={this.handleBrandChange}
handleComponentPick={this.handleComponentPick}
/>
<div className="currently-selected-component" />
<Route path="/:brand/button" component={Button} />
<Route path="/:brand/card" component={Card} />
</div>
);
};
}
I am wrapping the whole app in the Router.
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById("root")
);```
If you are getting this error inside a test using jest, you need to wrap your componen within a router. I am using react-testing-library, so my logic looks as follows:
import { render, cleanup } from '#testing-library/react'
import { BrowserRouter as Router } from 'react-router-dom'
import YourComponent from '../path/to/YourComponent'
// ...
describe('YourComponent component', () => {
afterEach(cleanup)
it('matches snapshot', () => {
const { asFragment } = render(
// The following will solve this issue
<Router>
<YourComponent />
</Router>
)
expect(asFragment()).toMatchSnapshot()
})
})
Can you try these following changes
handleComponentPick(event: any) { to handleComponentPick = (event: any) => {
then
render = () => { to render() {
Hope this works.
you have to pass the history like
<Router history={browserHistory} routes={routes} />,
that way, you can use history with props to navigate.
font: https://github.com/ReactTraining/react-router/blob/v3/docs/guides/Histories.md
try to use browserHistory on you app.js, like
render(
<Router history={browserHistory}>
<Route path='/' component={App}>
<IndexRoute component={Home} />
<Route path='about' component={About} />
<Route path='features' component={Features} />
</Route>
</Router>,
document.getElementById('app')
)
that way, you are passing history for all of your another router.
We need to pass history as a prop to Router. I am expecting that you are using react router v4 aka react-router-dom.
import { createBrowserHistory } from "history";
import { Router } from "react-router-dom";
const history = createBrowserHistory();
...
<Router history={history}>
<Routes />
</Router>
Demo : https://codesandbox.io/s/yv5y905ojv
Spied on the useHistory() hook and provided the mock route data.
import routeData from 'react-router';
describe('<Component /> container tests', () => {
beforeEach(() => {
const mockHistory = {
pathname: '/dashboard'
};
jest.spyOn(routeData, 'useHistory').mockReturnValue(mockHistory);
});

React: "history" prop is not available in a private route

I'm using React 16, React-router-dom 4 and Mobx in my app.
I have this code for a private route:
export default (props) => {
console.log('props from private',props)//Here i can see that the component doesn't contain the "history" prop.
const Component = props.component;
const match = props.computedMatch
if (isValidated()) {
return (
<div>
<div><Component {...props} match={match} /></div>
</div>
)
} else {
return <Redirect to="/login" />
}
};
This is the routing setup:
export const history = createHistory();
const AppRouter = () => (
<Router history={history}>
<Switch>
<PrivateRoute path="/" component={Chat} exact={true} />
<Route path="/login" component={Login} />
</Switch>
</Router>
);
For some reason, the history prop just doesn't exist in the private route, therefore i'm unable to use the this.props.history.push function, to redirect programatically. The prop does get passed to a "normal" route though.
What is wrong with my code?
Use below:
import {withRouter} from 'react-router-dom';
wrap component with withRouter.
withRouter(component_name)

How to reload a URL without refreshing the page in ReactJs?

I am trying to reload onto the same route without having to refresh the page. For this specific case, using history.pushState(), but I'm getting an error:
TypeError: history.pushState is not a function.
Here is my code:
import React from 'react';
import PropTypes from 'prop-types';
import { Container } from 'kawax-js';
import { Switch, Route } from 'react-router-dom';
import File from './FileContainer';
import Folder from './FolderContainer';
import HomeContainer from './HomeContainer';
class RootContainer extends React.Component {
static stateToProps = ({ ownProps, select }) => {
const files = select('files');
const lastFile = _.last(files);
return ({
lastFile: lastFile || {}
})
};
static propTypes = {
history: PropTypes.object.isRequired
};
static defaultProps = {
lastFile: {}
};
render() {
const { lastFile, history } = this.props;
if( lastFile === {} || !lastFile.isUploaded
|| lastFile.isUploaded === null) {
return (
<Switch>
<Route exact path="/" component={HomeContainer} />
<Route exact path="/file/:itemPath/:refHash" component={File} />
<Route exact path="/:folderName" component ={Folder}/>
</Switch>
);
}
return history.pushState(null, "/:folderName")
}
}
export default Container(RootContainer);
Is there a better way of doing this or am I missing something here?
You may get the desired result by forcing the component to rerender, take a look at the documentation here. I see you are extending React.Component so you should be able to do the following:
...
constructor() {
this.reload = this.reload.bind(this);
}
...
reload() {
this.forceUpdate();
}
...
I know it does not use history but there will be no other code required as it is included with the Component class.
please use this code
Router.browserHistory.push('/');
instaed of history.pushState(null, "/:folderName")
You have few possibilities to do that, currently my favorite way to do that is using anonymous function in component prop:
<Switch>
<Route exact path="/" component={()=><HomeContainer/>} />
<Route exact path="/file/:itemPath/:refHash" component={()=><File/>} />
<Route exact path="/:folderName" component ={()=><Folder/>}/>
</Switch>
Or if you want to refresh with current url params, you'll need extra route (reload), and play a little with router stack:
reload = ()=>{
const current = props.location.pathname;
this.props.history.replace(`/reload`);
setTimeout(() => {
this.props.history.replace(current);
});
}
<Switch>
<Route path="/reload" component={null} key="reload" />
<Route exact path="/" component={HomeContainer} />
<Route exact path="/file/:itemPath/:refHash" component={File} />
<Route exact path="/:folderName" component ={Folder}/>
</Switch>
<div onCLick={this.reload}>Reload</div>

Categories

Resources