Link collision in React Router - javascript

I'm trying to implement 'Not Found' page to redirect user if the url is not valid. However, not found page link somehow collides with search results and redirects user to 'Not Found' after submitting search input.
'Search', 'Result' and 'Not Found' are different components and links
are different.
Browser Router code in parent app component
(State for search results defined in here )
const [searchResult, setSearchResult] = useState([]);
<BrowserRouter>
<Switch>
<Route path=''>
<NotFound />
</Route>
<Route path='*'>
<Redirect to ='/404' />
</Route>
<Route path='/search'>
<Result
searchResult={searchResult}
setSearchResult={setSearchResult}
/>
</Route>
</Switch>
</BrowserRouter>
In the current situation, search bar component works and shows
results if I remove 'Not Found' from browser router.
Search results are displayed in 'Result' component.
Search Component
(If I remove first '/search', from history it doesn't work.)
Search result link
> http://localhost:3000/search/search?q=adidas
Item is available and can be found if I remove 'Not Found' from browser router. Else, paths collide and redirects to 'Not Found Component'
function Search({setSearchResult}) {
const history = useHistory();
const location = useLocation();
const searchInput = useRef();
const params = new URLSearchParams(location.search);
const q = params.get('q');
function handleSubmit(e){
e.preventDefault();
history.push(`/search/search?q=${searchInput.current.value}`)
}
useEffect(()=>{
if(q){
searchInput.current.value=q ? q : '';
const productSearch = products.results
.filter(item => item.title.toLowerCase().includes(q.toLowerCase()))
.map((item)=>
<Col sm={4} key={item.id} className="mt-3">
<Link to ={`/ProductDetails/${item.id}`} >
<Card>
<Card.Img variant="top" src={item.src[0]}/>
<Card.Body className="text-dark text-center">
<Card.Title className="font-secondary">{item.title}</Card.Title>
<Card.Text className="font-secondary">
{item.detail}
</Card.Text>
</Card.Body>
</Card>
</Link>
</Col>
);
setSearchResult(productSearch);
}
},[q])
return (
<>
<Form onSubmit={handleSubmit} inline>
<FormControl
htmlFor='search'
type="text"
id="search"
placeholder="Search items"
ref={searchInput}
name={q}
/>
<Button type='submit' id="searchBtn" onClick={handleSubmit} className="font-secondary" >{searchIcon}</Button>
</Form>
</>
)
}
export default Search
Result Component
Search results are displayed in here.
function Result({searchResult}) {
return (
<>
<Container>
<Row className="mt-5">
<h2 className="font-display">Search Results</h2>
</Row>
<Row>
{searchResult}
</Row>
</Container>
</>
)
}
export default Result
So I need to prevent that collision and make components work.

React Router will use the first component that it matches in the Switch statement, just like a regular switch statement.
So the NotFound and 404 components should be moved to the end of the Switch component, with search, and whatever other routes you're going to want, above them

Related

How to prevent PrivateRoutes stored in router stack from re-rendering after a user logs out?

I have an application with dynamic tabs that show different tabs depending if the user is authenticated or not. Naturally, I have created a PrivateRoute component to protect against users accessing certain pages while unauthenticated. However, I've discovered if the user logs in, visits an authenticated page, and then logs out, the PrivateRoute is still triggered even though the component no longer exists in DOM.
This is a problem because now that the user is not authenticated, the PrivateRoute still triggers and Redirects to the auth URL (which is the fallback for unauthenticated users in the PrivateRoute component).
Video example: https://streamable.com/qviin2
Code reproduction: https://codesandbox.io/s/pensive-voice-1w3rc?file=/src/App.tsx
Steps to reproduce:
Visit the Log In Tab
Click Log In
Click Tab 2
Click Log Out
Observe how you are unable to click to change any tab and how the private route is still triggered when changing tabs in the console.
(Also it's interesting to note that the Tab2 page shows, but the URL is incorrect and the tab that you click on doesn't change)
Any guidance on how to remove private routes from a router stack after logout (without a page refresh) would be GREATLY appreciated!! Thanks.
Here's my PrivateRoute:
const PrivateRoute: React.FC<PrivateRouteProps> = ({
component: Component,
...rest
}) => {
const { user, loading } = useAuth();
console.log("PRIVATE ROUTE [LOADING]", loading);
return (
<Route
{...rest}
render={(props) => {
return (
<>
{loading ? (
<Loading />
) : user ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: `/auth`,
state: {
from: props.location.pathname
}
}}
/>
)}
</>
);
}}
/>
);
};
My App.tsx contains the following routes:
<Route exact path="/tab2" component={Tab2} />
<Route path="/:tab(auth)" component={Login} exact />
<PrivateRoute path="/:tab(account)" component={Account} />
<PrivateRoute
path="/:tab(account)/settings"
component={Settings}
exact
/>
<Route exact path="/">
<Redirect to="/tab1" />
</Route>
On login I call an AuthProvider method to set a Userobject and route to aPrivateRoute`:
<IonButton
disabled={loading}
onClick={async () => {
await onAuthenticate();
push("/account");
}}
>
Log In
</IonButton>
And on Logout, I set the User object to undefined using the AuthProvider and try to route away from the PrivateRoute:
<IonButton
onClick={async () => {
await onLogout();
push("/tab2");
}}
>
Log out
</IonButton>

Rendering two different components with Reactjs

I've 2 different components, and I would like to render them separately. Logic is simple when user is logged in render 1st component, if it's not render default component.
Please review my code, and help me with this, I'm not good at react, so please help me
export default function App(props) {
const Authenticated = props.Authenticated
const RoutingCabinet = (
<React.Fragment>
<Header/>
<Switch>
<Route exact path = '/' component = { () => TempBody} />
<Route exact path = '/new' component = { () => <ResponsiveDrawer />}/>
</Switch>
<Footer/>
</React.Fragment>
);
const RoutingContent = (
<React.Fragment>
<Cabinet />
<Switch>
<Route exact path = '/user.test'component = { () => <div>Element</div> }/>
</Switch>
</React.Fragment>
);
return(
<ThemeProvider theme = {Theme}>
<BrowserRouter>
{
Authenticated
? RoutingCabinet
: RoutingContent
}
</BrowserRouter>
</ThemeProvider>
);
}
Currently I can see only RoutingCabinet
Here is the index.js
ReactDOM.render(<App Authenticated = {false} />, document.getElementById('__body__', '__root__'));
I may be wrong here, but is <Cabinet /> the same as <RoutingCabinet />? If so, it might work ok. The <div>Element</div> will be visible only if url matches user.test. If you are on any other page, this won't be rendered, so it'll look like <RoutingCabinet /> is rendered, while in fact it's <RoutingContent />, it just renders the same output.

How to detect matched route from a component outside of the <Route/> component that was matched using react-router?

I've got the following structure in my React app, using react-router-dom.
<Router>
<Header/>
<Main>
<AllRoutes> // THIS HANDLES THE SWITCH WITH ALL THE ROUTES
<Switch>
<Route exact path={ROUTES.HOME} component={Home}/>
<Route exact path={ROUTES.ABOUT} component={About}/>
<Route exact path={ROUTES.PRIVACY} component={Privacy}/>
// ETC
</Switch>
</AllRoutes>
</Main>
<Footer/> // <==== FOOTER NEEDS TO KNOW WHICH ROUTE HAS BEEN MATCH
<Router>
QUESTION
Footer needs to know what <Route/> has been match. What is the best pattern to achieve that?
OPTION #1
I found the useRouteMatch hook over on react router docs:
This would kind of work, but I don't think it is good enough for my situation. Because a URL string can match a route and still don't be a valid route at the same time.
For example:
Route: /:language/privacy
Valid route: /en/privacy
Not valid route that would also match: /notALanguage/privacy
Once a route has match, I usually need to check if it is valid before rendering a component page or the 404 page.
Like:
<Route exact path={"/:language/privacy"} render={(routeProps) => {
const possibleLanguage = routeProps.match.params.language;
if (possibleLanguage in LANGUAGES) {
return(
<PrivacyPage lang={possibleLanguage}/>
);
}
else {
return(
<Page404/>
);
}
}}/>
OPTION #2
What I'm thinking about doing is:
App.js calls useLocation. So it always re-render when there is a route change.
I could add a detectRoute function in App.js to do all the route checking beforehand.
And my AllRoutes component wouldn't need a component. I would implement a native JS switch and render the corresponding route.
This way I know upfront which <Route/> is going to match and I can pass it on to <Footer/> or any component that lives outside of the matched <Route/>.
Something like this:
SandBox Link
export default function App() {
console.log("Rendering App...");
const location = useLocation();
// THIS WOULD BE THE detectRoute FUNCTION
// I COULD EVEN USE THE useRouteMatch HOOK IN HERE
const matchedRoute =
location.pathname === ROUTE1
? "ROUTE1"
: location.pathname === ROUTE2
? "ROUTE2"
: "404";
return (
<div>
<div className="App">
<Link to={ROUTE1}>Route 1</Link>
<Link to={ROUTE2}>Route 2</Link>
<Link to={"/whatever"}>Route 404</Link>
</div>
<div>
<AllRoutes matchedRoute={matchedRoute} />
</div>
</div>
);
}
function AllRoutes(props) {
switch (props.matchedRoute) {
case "ROUTE1":
return <Route exact path={ROUTE1} component={Page1} />;
case "ROUTE2":
return <Route exact path={ROUTE2} component={Page2} />;
default:
return <Route exact path={"*"} component={Page404} />;
}
}
It works. But I would like to know if there's a proper way of doing this, 'cause this seems a bit weird and there might be something out there that was specifically designed for this.
Generally you want to either:
Wrap the components together
Create another switch to route them (and pass match params)
I put together a somewhat comprehensive example of the options. Hope that helps!
import React from "react";
import "./styles.css";
import { Switch, Link, Route, BrowserRouter } from "react-router-dom";
const hoc = (Component, value) => () => (
<>
<main>
<Component />
</main>
<Footer value={value} />
</>
);
const Wrapper = ({ component: Component, value }) => (
<>
<main>
<Component />
</main>
<Footer value={value} />
</>
);
const WrapperRoute = ({ component, value, ...other }) => (
<Route
{...other}
render={props => <Wrapper component={component} value={value} {...props} />}
/>
);
const Footer = ({ value }) => <footer>Footer! {value}</footer>;
const Header = () => <header>Header!</header>;
const Another = () => <Link to="/onemore">One More!</Link>;
const Home = () => <Link to="/other">Other!</Link>;
const OneMore = () => <Link to="/">Home!</Link>;
const Other = () => <Link to="/another">Another!</Link>;
export default () => (
<BrowserRouter>
<Header />
<Switch>
{/* You could inline it! */}
<Route
path="/another"
render={() => (
<>
<main>
<Another />
</main>
<Footer value="" />
</>
)}
/>
{/* You could use a custom route component (that uses an HOC or a wrapper) */}
<WrapperRoute
component={OneMore}
path="/onemore"
value="I got one more!"
/>
{/* You could use a Higher-Order Component! */}
<Route path="/other" component={hoc(Other, "I got other!")} />
{/* You could use a wrapper component! */}
<Route
path="/"
render={() => <Wrapper component={Home} value="I got home!" />}
/>
</Switch>
{/* You could have another switch for your footer (inline or within the component) */}
<Switch>
<Route
path="/another"
render={() => <Footer value="Switch footer another!" />}
/>
<Route
path="/other"
render={() => <Footer value="Switch footer other!" />}
/>
<Route
path="/onemore"
render={() => <Footer value="Switch footer onemore!" />}
/>
<Route path="/" render={() => <Footer value="Switch footer home!" />} />
</Switch>
</BrowserRouter>
);
Note the WrapperRoute would allow you to do validation on your match params before passing them through. You could do a Redirect if needed.
What I've ended up doing:
Since I'm using Redux, I added a piece of global state to keep track of the matched route.
And I dispatch actions to update that state from the render prop from the <Route/>'s component.
<Switch>
<Route key={index} exact path={"/some-route"} render={(routeProps) => {
// HERE I DISPATCH AN ACTION TO CHANGE THE STATE FOR THE CURRENT ROUTE
dispatch({
type: UPDATE_CURRENT_ROUTE,
payload: { name: "SOME_ROUTE_NAME" }
});
return (
<PrivacyPage
{...routeProps}
/>
);
}}/>
</Switch>
And now I can do on Footer.js:
function Footer() {
const currentRoute = useSelector((state) => state.currentRoute);
// RENDER FOOTER ACCORDINGLY TO THE CURRENT ROUTE
}

How to change NavBar text on login/logout in React.JS?

I have a Navigation bar in my project which I call from inside App.js. Based on if I am logged in or not, I want to render different views of NavBar. If logged in, I want the NavBar to have a logout button. And if logged out, I want the NavBar to have login button. I use a token in localStorage to check if I am logged in or not. When logged in, token is present in localStorage. On logout/before login, there is no token key in localStorage. I pass this token as a state to NavBar as shown:
export default function App() {
const [loggedIn, setLoggedIn] = useState(localStorage.getItem("token"));
return (
<div>
<Router>
<Navbar isAuth={loggedIn} />
<Route exact path="/" exact component={Home} />
<Route path="/login" component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
</Router>
</div>
);
}
Now from NavBar component, I use this prop to render different views of NavBar as shown below:
const NavBar = props => {
const classes = useStyles();
if (props.isAuth !== null) {
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={classes.title}>
<Link
href="/"
style={{ textDecoration: "none", color: "white" }}
>
Timetracker
</Link>
</Typography>
<Link href="/" style={{ color: "white" }}>
<Button color="inherit" onClick={auth.logout}>
Logout
</Button>
</Link>
</Toolbar>
</AppBar>
</div>
);
} else {
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={classes.title}>
<Link
href="/"
style={{ textDecoration: "none", color: "white" }}
>
Timetracker
</Link>
</Typography>
<Link href="/login" style={{ color: "white" }}>
<Button color="inherit">Login</Button>
</Link>
</Toolbar>
</AppBar>
</div>
);
}
};
export default NavBar;
The problem is that, the NavBar does not update itself as soon as I login. I have to manually refresh the page in order to render the new NavBar. Similarly on logout too, It does not update itself and updates only on manual refresh. What is the issue and how to solve this?
I found a simple solution:
use a componentDidMount() or useEffect() function which will render automatically upon loading the NavBar page.
Inside this function, use a setInterval() function to continually retrieve the auth status (say, an interval of 5000). This will continually refresh the NavBar, and change the button immediately.
I imagine you would have to put the auth check in the NavBar component itself, instead of using props. I put the specific buttons I wanted to change in a separate component called NavBarUser, which changes 'login | signup' to 'logout' and contains a user avatar. I then inserted this component into the NavBar itself at the appropriate place.
This is what my code looks like:
```
import React, { useState, useEffect } from 'react';
import Avatar from './Avatar';
import { BrowserRouter as Router, Link } from 'react-router-dom';
const NavBarUser = () => {
const [user, setUser] = useState({});
useEffect(() => {
{ /*
setInterval was used in order to refresh the page constantly
in order to have the "logout" button show immediately in place of
"login", as soon as user logs out.
*/}
setInterval(() => {
const userString = localStorage.getItem("user");
const user = JSON.parse(userString);
setUser(user);
}, [])
}, 5000);
const logout = () => {
return localStorage.removeItem("user");
}
if (!user) {
return (
<div className="navbar-nav ml-auto">
<Link to="/login" className="nav-item nav-link">Login</Link> <span
className="nav-item nav-link">|</span> <Link to="/SignUp" className="nav-item nav-
link">Sign Up</Link>
</div>
)
}
if (user) {
return (
<div className="navbar-nav ml-auto">
<Link to="/" className="nav-item nav-link" onClick={logout}>Logout</Link>
<Avatar img="/images/Eat-healthy.jpg" />
</div>
)
}
}
export default NavBarUser;
```
You need to add <Switch> as well. From the documentation:
Renders the first child or that matches the location.
<Switch> is unique in that it renders a route exclusively. In contrast, every <Route> that matches the location renders inclusively.
Just like the following:
<Router>
<Switch>
<Navbar isAuth={loggedIn} />
<Route exact path="/" exact component={Home} />
<Route path="/login" component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
</Switch>
</Router>
Read further here: Router
I hope that helps!
Your app's state won't update if you change the value of the token in localStorage.
You need to make sure you update the state, I've added a sandbox if it helps.
Here's how I solved this issue:
To start, I created a isLoggedIn state in my App class. I gave it a componentDidMount() method that would fetch the login state from a cookie on app start. Then I created globalLogin and globalLogout methods as arrow functions, which set the isLoggedIn state to true or false accordingly. I passed my Nav component the isLoggedIn state as a prop and passed the Login and Nav routes the globalLogin and globalLogout methods. These methods can then be called from Login or Nav with this.props.globalLogout(); or this.props.globalLogin();.
This is a simplified version of my App.js.
class App extends Component {
constructor(props){
super(props);
this.state = {
isLoggedIn: false,
}
}
componentDidMount() {
const token = Cookie.get("token") ? Cookie.get("token") : null;
if (token) {
this.setState({ "isLoggedIn": true });
}
}
globalLogin = () => {
this.setState({ "isLoggedIn": true });
}
globalLogout = () => {
this.setState({ "isLoggedIn": false });
}
render() {
return (
<Router>
<div className="App">
<Nav isLoggedIn={this.state.isLoggedIn} globalLogout={this.globalLogout}/>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/login" exact>
<Login globalLogin={this.globalLogin}/>
</Route>
</Switch>
</div>
</Router>
);
}
}
EDIT: using history.push didn't work in login module above so I added an intermediate to handle props
render() {
const LoginIntermediate = (props) => {
return (
<Login {...props} globalLogin={this.globalLogin}/>
)
}
return (
<Router>
<div className="App">
<Nav isLoggedIn={this.state.isLoggedIn} globalLogout={this.globalLogout}/>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/login" exact component={LoginIntermediate} />
</Switch>
</div>
</Router>
);
}

How to map routes with individual ListItem in reactjs?

Im trying to use material Ui with my react frontend project. I have implemented the AppBar and drawer component and they are working just fine. This is what I have achieved till now :
export default function MiniDrawer() {
const classes = useStyles();
const theme = useTheme();
const [open, setOpen] = React.useState(false);
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
return (
<div className={classes.root}>
<CssBaseline />
<AppBar style={{background: 'black'}}
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
className={clsx(classes.menuButton, {
[classes.hide]: open,
})}
>
<MenuIcon />
</IconButton>
<Typography className={classes.mystyle} variant="h5" >
CodeBasics
</Typography>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
open={open}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
</IconButton>
</div>
<Divider />
<List>
{['Home', 'Algorithms', 'DataStructures'].map((text, index) => (
<ListItem button key={text}>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
<Divider />
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
<Layout>
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/DataStructures" component={DataStructures} />
</Switch>
</Router>
</Layout>
</main>
</div>
);
}
Now I want to map each 'Home', 'DataStructures' and 'Algorithms' with a different route. I have achieved a way to a route the List component in this way.
<List>
{['Home', 'Algorithms', 'DataStructures'].map((text, index) => (
<ListItem button key={text} component="a" href="www.google.com">
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
but with its only getting me go to the same link for every item in the list. How do I map individual list items with individual routes?
I've put together a small sample. This structure works for me for all of my projects and quite fast to implement once you've got the hang of it.
Demo link
Route Structure
This pathIds variable is an object which holds all id values for routes. This will be useful when needing a reference to auto generate breadcrumb links.
const pathIds = {
home: 'home',
...
}
This pathRouting variable is an object which holds all path routing. Just path routing.
const pathRouting = {
home: '/home',
...
}
Primary structure object:
const pageRoutes = {
[pathIds.home]: {
path: pathRouting.home,
sidebarName: 'Homepage',
icon: Dashboard,
noRender: false,
component: Home,
},
...
}
This is primary structure object, holding all necessary info to generate onto HTML (sidebar, breadcrumbs, routes etc).
path: get the path value from pathRouting object. (Required)
sideName: text showing on Sidebar. (Required)
icon: usually this icon only shows on the sidebar. You can omit this if your menu/sidebar/navigation doesn't need it. (Optional)
noRender: boolean to indicate whether this object will be rendered to the sidebar. This is useful for routes that doesn't need to directly access (like a 404 Page Not Found page) (Optional)
component: The React component which will be used to render. (Required)
Then, in your app.js, you import pageRoutes as usual. Before doing mapping or iteration work, you might need to convert this object to an array (or use a library like lodash to directly iterating on the object).
const routeArray = Object.values(pageRoutes);
Then:
<Switch>
{routeArray.map((prop, key) => {
return (
<Route
path={prop.path}
component={prop.component}
exact={prop.exact || false}
key={`route-${key}`}
/>
);
})}
<Route component={pageRoutes[pathIds.error404].component} />
</Switch>
Notice the very last line where I explicitly declare a fallback <Route /> path in case user is on a wrong/ not defined path, that's how useful an object is in this kind of situation. It doesn't need path prop.
You sidebar is just a simple component receives a list of routes as props (as converted in app.js), then it can be used to show on the view.
<List component="nav" aria-label="main mailbox folders">
{routes.map(({ path, noRender, sidebarName, ...prop }, index) => {
if (noRender) return null;
return (
<NavLink to={path} key={`route-${index}}`}>
<ListItem button>
<ListItemIcon>
<prop.icon />
</ListItemIcon>
<ListItemText primary={sidebarName} />
</ListItem>
</NavLink>
);
})}
</List>
You will see it only renders Homepage, Inbox and Reset Password on sidebar. But you can directly enter /register to make <Register /> show up. The same for /page-not-found. Or even when you enter a wrong address like /register-user, the <PageNotFound /> component will be used for fallback case.
You might wonder why I don't put all path routes and path Ids in its own place, why do I need to move them into a separate object? The answer is, in my case I usually need quick access to those values. This might be useful for generating breadcrumbs or do some iteration without pulling the whole object. Depend on your specific project needs, those two might be shortened.
For full code, you can visit here: Sample Link
I have found a way to achieve this.
First I created an array of objects like:
const topics= [{
topic: "Home",
path: "home"
},
{
topic: "DataStructures",
path: "datastructures"
}]
then I did this:
<List>
{topics.map((text, index) => (
<ListItem button key={text.topic} component="a" href={text.path === "home"? "\\":text.path}>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text.topic} />
</ListItem>
))}
</List>

Categories

Resources