How to prevent components from unmounting when path changes? - javascript

Is there a way to stop the route from unmounting? In my app I would need to keep react state between switching the routes.
Here you can see that, if we change route then home respective route component will mount and unmount. I don't want.
CODESANDBOX DEMO
I've looked in
How can I prevent the unmount in React Components?
How can I prevent React from unmounting/remounting a component?
but doesn't work for me.
index.js
import React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import App from "./App";
import First from "./routes/First";
import Second from "./routes/Second";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<BrowserRouter>
<Routes>
<Route path="" element={<App />} />
<Route path="first" element={<First />} />
<Route path="second" element={<Second />} />
</Routes>
</BrowserRouter>
);
App.js
import * as React from "react";
import { Link } from "react-router-dom";
export default function App() {
return (
<div>
<h1>Prevent Routing</h1>
<nav
style={{
borderBottom: "solid 1px",
paddingBottom: "1rem"
}}
>
<Link to="/first">First</Link> |<Link to="/second">Second</Link>
</nav>
</div>
);
}
First.js
import * as React from "react";
import { useEffect } from "react";
import { Link } from "react-router-dom";
export default function First() {
useEffect(() => {
console.log("mounted First");
return () => console.log("unmounted First");
}, []);
return (
<main style={{ padding: "1rem 0" }}>
<Link to="/">Home</Link>
<h2>First</h2>
</main>
);
}
Second.js
import * as React from "react";
import { useEffect } from "react";
import { Link } from "react-router-dom";
export default function Second() {
useEffect(() => {
console.log("mounted Second");
return () => console.log("unmounted Second");
}, []);
return (
<main style={{ padding: "1rem 0" }}>
<Link to="/">Home</Link>
<h2>Second</h2>
</main>
);
}

React Router will always unmount the component when it's not matched by a path.
You could fix this by combining both Components with a double route, each component can determine what he will render based on the current path.
So the main routing will look something like:
<Route path={[ '/first', '/second' ]}>
<First {...{}} />
<Second {...{}} />
</Route>
Then use useLocation in the component to toggle the render:
import { useLocation } from "react-router-dom";
function First(props) {
const location = useLocation();
if (location.pathname !== '/first') {
return null
}
}

Related

Why Route tag can't render the component in React?

App.js code-
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Container } from "react-bootstrap";
import Header from "./components/Header";
import Footer from "./components/Footer";
import HomeScreen from "./screens/HomeScreen";
const App = () => {
return (
<Router>
<Header />
<main className='py-3'>
<Container>
<HomeScreen />
</Container>
</main>
<Footer />
</Router>
);
};
export default App;
My app.js was like this and everything was working fine. But after that I added Route tag and the code become like this
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Container } from "react-bootstrap";
import Header from "./components/Header";
import Footer from "./components/Footer";
import HomeScreen from "./screens/HomeScreen";
const App = () => {
return (
<Router>
<Header />
<main className='py-3'>
<Container>
<Route exact path='/' component={HomeScreen} />
</Container>
</main>
<Footer />
</Router>
);
};
export default App;
But after adding Route, HomeScreen is not rendering?
Can anybody tell me why this is happening?
In react-router-dom v6 the Route component must be rendered into a Routes component, and the routed components are rendered on the element prop as a ReactElement (a.k.a. JSX) since the component, and render and children function props no longer exist.
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { Container } from "react-bootstrap";
import Header from "./components/Header";
import Footer from "./components/Footer";
import HomeScreen from "./screens/HomeScreen";
const App = () => {
return (
<Router>
<Header />
<main className='py-3'>
<Container>
<Routes>
<Route path='/' element={<HomeScreen />} />
</Routes>
</Container>
</main>
<Footer />
</Router>
);
};

React Router doesn't render

i've been following an online course about react but it's a bit dated and i've been struggling through all the lessons to understand the concepts explained while applying the new syntax instead of the the one shown in the videos. i've always more or less managed but now i'm stuck because, after adding the react router dom web app, my pages don't render anymore. no errors nor warnings are shown and no matter how much i look, i can't seem to find the mistake. my main right now looks like this:
import React from 'react';
import Header from './header';
import MyPlacesList from './myplaceslist.js';
import Footer from './footer';
import CreatePlace from './createPlace.js';
import * as attAPI from '../utils/attractionsAPI';
import { Router, Route } from 'react-router-dom';
class Main extends React.Component{
state = {
attractions: [],
}
componentDidMount(){
attAPI.getAll().then((attractions) => {
this.setState({attractions})
})
}
removePlace = (attraction) => {
this.setState((state) => ({
attractions: state.attractions.filter((attr) => attr.id !== attraction.id)
}))
attAPI.remove(attraction);
}
render(){
return (
<Router>
<Fragment>
<Header titolo = 'My Places' sottotitolo = 'I miei posti preferiti'/>
<Route exact path = '/' render = {() => (
<MyPlacesList attractions = {this.state.attractions}
onRemovePlace = {this.removePlace} />
)}/>
<Route path = '/create' render = {() => (
<CreatePlace />
)}/>
<Footer />
</Fragment>
</Router>
)
}
}
export default Main;
and this is the header:
import React from 'react';
import AppBar from '#mui/material/AppBar';
import Box from '#mui/material/Box';
import Toolbar from '#mui/material/Toolbar';
import Typography from '#mui/material/Typography';
import IconButton from '#mui/material/IconButton';
import Icon from '#mui/material/Icon';
import AddCircleIcon from '#mui/icons-material/AddCircle';
import { Link } from 'react-router-dom';
class Header extends React.Component{
render(){
// return <nav>
// <div className = "nav-wrapper">
// {this.props.titolo}
// </div>
// </nav>
return <Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<Typography
variant="h6"
noWrap
component="div"
sx={{ flexGrow: 1, display: { xs: 'none', sm: 'block' } }}
>
{this.props.titolo}
</Typography>
<Link to = '/create'>
<IconButton>
<AddCircleIcon />
</IconButton>
</Link>
</Toolbar>
</AppBar>
</Box>
}
}
export default Header;
before adding react router dom i was using the state to call the components MyPlacesList/CreatePlace (createPlace only gets shown when i click on a button) and it worked just fine, the problem appeared after i tried to use <Route>. any ideas in how could i fix this? thank you in advance!
EDIT - here's my index page too:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Main from './components/main';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<Main/>
</BrowserRouter>, document.getElementById('root'));
Things to correct
Use React.Fragment instead of just Fragment or import from react.
Add All Routes between Routes component.
You have already add Router or BrowserRouter in index.js so don't need to add again.
There is a change in nesting in version 5.
So ultimately the nesting should be in version 6
<BrowserRouter>
<Routes>
<Route>
<Route>
</Routes>
</BrowserRouter>
import MyPlacesList from "./myPlacesList.js";
import CreatePlace from "./createPlace.js";
import { Route, Routes } from "react-router-dom";
class App extends React.Component {
render() {
return (
<React.Fragment>
<div>Some Header component</div>
<Routes>
<Route
exact
path="/"
render={() => (
<MyPlacesList
attractions={this.state.attractions}
onRemovePlace={this.removePlace}
/>
)}
/>
<Route path="/create" render={() => <CreatePlace />} />
</Routes>
</React.Fragment>
);
}
}
export default App;

React router - url changes, no render

Using a simple link to / route to via React browser Router.
I got it to work fine with routing to the root component (/), however it does not function as expected when trying to route to (/drink/:drinkId), though the URL changes and the page loads if I manually try to access it.
App component:
import React from "react";
import "./App.css";
import Cocktails from "./Cocktails";
import { Container } from "react-bootstrap";
import NavApp from "./Navbar";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import DrinkDetails from "./DrinkDetails";
import "./App.css";
function App() {
return (
<Router>
<Container className="App-container">
<NavApp />
<Switch>
<Route exact path="/">
<Cocktails size={20} />
</Route>
<Route exact path="/drink/:drinkId">
<DrinkDetails />
</Route>
</Switch>
</Container>
</Router>
);
}
export default App;
Drink details component:
import { React, useState, useEffect } from "react";
import { Jumbotron, Button } from "react-bootstrap";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useParams,
} from "react-router-dom";
import axios from "axios";
function DrinkDetails() {
let { drinkId } = useParams();
const [drink, setDrink] = useState(null);
const [ingrdts, setIngrdts] = useState([]);
useEffect(() => {
const getDrinkSpecs = async () => {
const res = await axios.get(
`https://www.thecocktaildb.com/api/json/v1/1/lookup.php?i=${drinkId}`
);
let newDrink = res.data.drinks[0];
setDrink(newDrink);
let newIngrdts = [];
for (let i = 1; i <= 15; i++) {
if (newDrink[`strIngredient${i}`] != null) {
let ingrdtVal = {};
ingrdtVal["ing"] = newDrink[`strIngredient${i}`];
ingrdtVal["val"] = newDrink[`strMeasure${i}`];
newIngrdts.push(ingrdtVal);
}
}
setIngrdts([...newIngrdts]);
};
getDrinkSpecs();
}, [drinkId]);
return drink ? (
<Jumbotron>
<h1>
{drink.strDrink}
<img src={drink.strDrinkThumb} />
</h1>
<p>Glass: {drink.strGlass}</p>
<p>Category: {drink.strCategory}</p>
<p>Instructions: {drink.strInstructions}</p>
{ingrdts.map((ingrdt) => (
<p>
{ingrdt.ing} : {ingrdt.val}
</p>
))}
<p>
<Button variant="primary">Learn more</Button>
</p>
</Jumbotron>
) : (
<Jumbotron>
<h1>Hmmm... we don't have this yet!</h1>
<p>
This is a simple hero unit, a simple jumbotron-style component for
calling extra attention to featured content or information.
</p>
<p>
<Button variant="primary">Learn more</Button>
</p>
</Jumbotron>
);
}
export default DrinkDetails;
and this is where I use Link:
import React from "react";
import { Button, Card } from "react-bootstrap";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import "./Card.css";
function CardDrink({ data, select }) {
return (
<Router>
<Card className="Cocktail-card">
<Link to={`/drink/${data.idDrink}`}>
<Card.Img
variant="top"
src={data.strDrinkThumb}
className="Cocktail-card-img"
onClick={() => select(data.idDrink)}
/>
</Link>
<Card.Body>
<Card.Title>{data.strDrink}</Card.Title>
</Card.Body>
</Card>
</Router>
);
}
export default CardDrink;
Remove <Router> from CardDrink component. You need only one Router at root level which you already have in App component.
Also, as a practice don't keep unused imports in your component. I see Router imported in DrinkDetails component as well.
From docs
To use a router, just make sure it is rendered at the root of your element hierarchy. Typically you’ll wrap your top-level element in a router.
Try to use the useHistory() hook:
import React from "react";
import { Button, Card } from "react-bootstrap";
import { BrowserRouter as Router, Switch, Route, Link, useHistory } from "react-router-dom";
import "./Card.css";
function CardDrink({ data, select }) {
const history = useHistory()
return (
<Router>
<Card className="Cocktail-card">
<div onClick={()=>history.push(`/drink/${data.idDrink}`)}>
<Card.Img
variant="top"
src={data.strDrinkThumb}
className="Cocktail-card-img"
onClick={() => select(data.idDrink)}
/>
</div>
<Card.Body>
<Card.Title>{data.strDrink}</Card.Title>
</Card.Body>
</Card>
</Router>
);
}
export default CardDrink;

Url change but not rendering component React Router

Hi i am trying to change the route of my website to go to Contact, when the menu is clicked.
Whenever i press the button for the homepage which is "Forside", the routing works perfectly.
But as soon as i press the button for the contact which is "Kontakt", the url changes and no component renders. But if i refresh the page, it shows up..
Any ideas what is wrong with my code, or any way to fix it?
All help will be appreciated thanks.
My App.js
import React from 'react';
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import Forside from './forside';
import Kontakt from './Kontakt';
import Header from './Header'
const App = () => {
return(
<Router>
<div>
<Header/>
<Switch>
<Route exact path="/" component={Forside}/>
<Route path="/kontakt" component={Kontakt}/>
</Switch>
</div>
</Router>
)
}
export default App
And this is where i link to the different routes.
import React, { useState } from 'react';
import './header.css'
import { makeStyles } from '#material-ui/core/styles';
import BottomNavigation from '#material-ui/core/BottomNavigation';
import BottomNavigationAction from '#material-ui/core/BottomNavigationAction';
import ContactMailIcon from '#material-ui/icons/ContactMail';
import LocationOnIcon from '#material-ui/icons/LocationOn';
import {Link} from 'react-router-dom';
const useStyles = makeStyles({
root: {
width: 500,
},
});
const Header = () => {
const classes = useStyles();
const [value, setValue] = React.useState(0);
return(
<div className="hed">
<h1 className="logo"><span>IT</span> ARBEJDE.DK</h1>
<BottomNavigation
value={value}
className="nav"
onChange={(event, newValue) => {
setValue(newValue);
}}
showLabels
className={classes.root}
>
<Link to="/">
<BottomNavigationAction label="Søg job" icon={<LocationOnIcon />} />
</Link>
<Link to="/kontakt">
<BottomNavigationAction label="Kontakt" icon={<ContactMailIcon/>} />
</Link>
</BottomNavigation>
</div>
)
}
export default Header
add exact to it to your route
<Route exact path="/kontakt" component={Kontakt}/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Since i didn't get a answer, i have figured it out on my own. So i would just like to share the solution, for other people who maybe are expecting the same bug.
I fixed the problem, by restructuring my code.
I placed my router inside the index.js component like this:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import {BrowserRouter as Router} from 'react-router-dom';
ReactDOM.render(
<React.StrictMode>
<Router>
<App/>
</Router>
</React.StrictMode>,
document.getElementById('root')
);
Then placed the Header code inside my App.js, instead of having the Component rendering.
So i refactored the code like this:
import React from 'react';
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import Kontakt from './Kontakt';
import Forside from './forside';
import { makeStyles } from '#material-ui/core/styles';
import BottomNavigation from '#material-ui/core/BottomNavigation';
import BottomNavigationAction from '#material-ui/core/BottomNavigationAction';
import ContactMailIcon from '#material-ui/icons/ContactMail';
import LocationOnIcon from '#material-ui/icons/LocationOn';
import {Link} from 'react-router-dom';
import './header.css'
const useStyles = makeStyles({
root: {
width: 500,
},
});
const App = () => {
const classes = useStyles();
const [value, setValue] = React.useState(0);
return(
<div>
<div className="header-container">
<h1 className="logo"><span>IT</span> ARBEJDE.DK</h1>
<BottomNavigation
value={value}
className="nav"
onChange={(event, newValue) => {
setValue(newValue);
}}
showLabels
className={classes.root}
>
<BottomNavigationAction label="Søg job" component={Link} to="/" icon={<LocationOnIcon />} />
<BottomNavigationAction label="Kontakt" component={Link} to="/kontakt" icon={<ContactMailIcon/>} />
</BottomNavigation>
</div>
<Switch>
<Route exact path="/" component={Forside}/>
<Route exact path="/kontakt" component={Kontakt}/>
</Switch>
</div>
)
}
export default App

Is there a way to stop my header from re-rendering when navigating my site?

I'm creating a website using React JS, React-Router-Dom, React-Redux & React-Persist.
I set up a login & sign up page with firebase. When a user logs in or signs up, I would like to have their display name in the header-component. Using my current method, I realized that when a user is logged in, the header component re-renders every time, however, I'm not too fond of that since it doesn't make navigating my website smooth. When the user is not logged in, the header component doesn't re-render.
I'm relatively new to React JS and was reading the documentation & googling other similar problems, but I can not find a solution and having a hard time approaching this. Any assistance or suggestions would be greatly appreciated!
Below are my index.js, App.js, header.component.jsx
index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import "bootstrap/dist/css/bootstrap.css";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { store, persistor } from "./redux/store";
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<PersistGate persistor={persistor}>
<App />
</PersistGate>
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
App.js
import React from "react";
import { Route, Switch, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import "./App.css";
import Header from "./components/header/header.component";
import Footer from "./components/footer/footer.component";
import HomePage from "./pages/homepage/homepage.page";
import SponsorsPage from "./pages/sponsors/sponsors.page";
import TeamPage from "./pages/team/team.page";
import AccountPage from "./pages/myaccount/myaccount.page"
import SignInAndSignUpPage from "./pages/sign-in-and-sign-up/sign-in-and-sign-up.page";
import { auth, createUserProfileDocument } from "./firebase/firebase.util";
import { setCurrentUser } from "./redux/user/user.action";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "./redux/user/user.selector";
class App extends React.Component {
unSubscribeFromAuth = null;
componentDidMount() {
const { setCurrentUser } = this.props;
this.unSubscribeFromAuth = auth.onAuthStateChanged(async (userAuth) => {
if (userAuth) {
const userRef = await createUserProfileDocument(userAuth);
userRef.onSnapshot((snapShot) => {
setCurrentUser({
id: snapShot.id,
...snapShot.data(),
});
});
}
setCurrentUser(userAuth);
});
}
componentWillUnmount() {
this.unSubscribeFromAuth();
}
render() {
return (
<div>
<Header />
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/sponsors" component={SponsorsPage} />
<Route exact path="/team" component={TeamPage} />
<Route exact path="/myaccount" component={AccountPage} />
<Route
exact
path="/signin"
render={() =>
this.props.currentUser ? <Redirect to="/" /> : <SignInAndSignUpPage/>
}
/>
</Switch>
<Footer />
</div>
);
}
}
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
setCurrentUser: (user) => dispatch(setCurrentUser(user)),
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
header.component.jsx
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selector";
import { auth } from "../../firebase/firebase.util";
import { ReactComponent as Logo } from "../../assets/eaglerocketry-icon.svg";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./header.style.scss";
const Header = ({ currentUser }) => (
<Navbar collapseOnSelect expand="md" bg="light" fixed="top">
<Navbar.Brand className="mr-auto">
<Logo className="logo" />
</Navbar.Brand>
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
<Navbar.Collapse id="responsive-navbar-nav">
<Nav className="pl-md-4">
<Nav.Link href="/">HOME</Nav.Link>
<Nav.Link href="/outreach">OUTREACH</Nav.Link>
<Nav.Link href="/team">TEAM</Nav.Link>
<Nav.Link href="/sponsors">SPONSORS</Nav.Link>
</Nav>
<Nav className="ml-auto">
{currentUser ? <Nav.Link href="/myaccount">{currentUser.displayName}</Nav.Link> : null}
{currentUser ? (
<Nav.Link onClick={() => auth.signOut()}>SIGN OUT</Nav.Link>
) : (
<Nav.Link href="/signin">SIGN IN</Nav.Link>
)}
</Nav>
</Navbar.Collapse>
</Navbar>
);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
export default connect(mapStateToProps)(Header);
Every time that a prop (in this case currentUser) changes in a stateless component (like your Header component) is gonna re-render, because you are using that prop inside or your component, not only for displaying the displayName, but also to conditionally render some links ( <Nav.Link> ). So it is inevitable that a react component doesn't re-render if its props change.
being relatively new React JS I found a solution to my question.
I had to make my functional component into a class and have a local state that stores the name & use that in my render() function instead of this.props.currentUser.displayName.
class Header extends React.Component {
constructor(props){
super(props);
this.state = {
name: this.props.currentUser.displayName,
};
}

Categories