React Router V6 Re-renders on Route Change - javascript

When I navigate back and forth between routes, React Router re-renders memoized routes causing useEffect(() => []) to re-run and data to re-fetch. I'd like to prevent that and instead keep existing routes around but hidden in the dom. I'm struggling with "how" though.
The following is sample code for the problem:
import React, { useEffect } from "react";
import { BrowserRouter as Router, Route, Routes, useNavigate } from "react-router-dom";
export default function App() {
return (
<Router>
<Routes>
<Route path={"/"} element={<MemoizedRouteA />} />
<Route path={"/b"} element={<MemoizedRouteB />} />
</Routes>
</Router>
);
}
function RouteA() {
const navigate = useNavigate()
useEffect(() => {
alert("Render Router A");
}, []);
return (
<button onClick={() => { navigate('/b') }}>Go to B</button>
);
};
const MemoizedRouteA = React.memo(RouteA)
function RouteB() {
const navigate = useNavigate()
useEffect(() => {
alert("Render Router B");
}, []);
return (
<button onClick={() => { navigate('/') }}>Go to A</button>
);
}
const MemoizedRouteB = React.memo(RouteB)
Sandbox: https://codesandbox.io/s/wonderful-hertz-w9qoip?file=/src/App.js
With the above code, you'll see that the "alert" code is called whenever you tap a button or use the browser back button.
With there being so many changes of React Router over the years I'm struggling to find a solution for this.

When I navigate back and forth between routes, React Router re-renders
memoized routes causing useEffect(() => []) to re-run and data to
re-fetch. I'd like to prevent that and instead keep existing routes
around but hidden in the dom. I'm struggling with "how" though.
Long story short, you can't. React components rerender for one of three reasons:
Local component state is updated.
Passed prop values are updated.
The parent/ancestor component updates.
The reason using the memo HOC doesn't work here though is because the Routes component only matches and renders a single Route component's element prop at-a-time. Navigating from "/" to "/b" necessarily unmounts MemoizedRouteA and mounts MemoizedRouteB, and vice versa when navigating in reverse. This is exactly how RRD is intended to work. This is how the React component lifecycle is intended to work. Memoizing a component output can't do anything for when a component is being mounted/unmounted.
If what you are really trying to minimize/reduce/avoid is duplicate asynchronous calls and data fetching/refetching upon component mounting then what I'd suggest here is to apply the Lifting State Up pattern and move the state and useEffect call into a parent/ancestor.
Here's a trivial example using an Outlet component and its provided context, but the state could be provided by any other means such as a regular React context or Redux.
import React, { useEffect, useState } from "react";
import {
BrowserRouter as Router,
Route,
Routes,
Outlet,
useNavigate,
useOutletContext
} from "react-router-dom";
export default function App() {
const [users, setUsers] = useState(0);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then(setUsers);
}, []);
return (
<Router>
<Routes>
<Route element={<Outlet context={{ users }} />}>
<Route path={"/"} element={<RouteA />} />
<Route path={"/b"} element={<RouteB />} />
</Route>
</Routes>
</Router>
);
}
function RouteA() {
const navigate = useNavigate();
return (
<div>
<button onClick={() => navigate("/b")}>Go to B</button>
</div>
);
}
function RouteB() {
const navigate = useNavigate();
const { users } = useOutletContext();
return (
<div>
<button onClick={() => navigate("/")}>Go to A</button>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} : {user.email}
</li>
))}
</ul>
</div>
);
}

Related

How To Pass Props To Function Component use react-router-dom v6

I want to pass props into a function from the route, my code currently looks like this
Route
<Route path="/person" render={(params) => <ProductDetails {...params} />} />
Code
function ProductDetails(props) {
return (
<div className="section has-text-centered mt-4">
console.log(props)
<h1>Hello</h1>
</div>
);
};
export default ProductDetails;
What am I missing here, please help.
In react-router-dom v6 you pass props to the routed component directly since they are passed as a ReactElement (a.k.a. JSX).
Example:
<Route path="person" element={<ProductDetails /* pass props here */ />} />
Based on your example snippet though, it sounds like your question it more specifically about passing the "route props" to a component. In RRDv6 there are no longer any route props, i.e. no history, location, and match props. Use the React hooks to access these: useNavigate for navigation, useLocation for location, and useMatch for match and useParams specifically for the route params.
import { useNavigate, useLocation, useParams } from 'react-router-dom';
function ProductDetails(props) {
const navigate = useNavigate();
const location = useLocation();
const match = useMatch();
useEffect(() => {
console.log(props);
}, [props]);
return (
<div className="section has-text-centered mt-4">
<h1>Hello</h1>
</div>
);
};
export default ProductDetails;
You can pass function with props in React Router DOM V6 in this way:
<Route path="/person" element={<ProductDetails render={(params) => ({ ...params })} />} />
And you can pass more props as names outside of render function.
Example:
<Route path="/person" element={<ProductDetails render={(params) => ({ ...params })} user={user} />} />

React-router-dom Render props isn't returning any component

I'm using react-router-dom version 6.0.2 here and the "Render" props isn't working, every time I got to the url mentioned in the Path of my Route tag it keeps throwing me this error - "Matched leaf route at location "/addRecipe" does not have an element. This means it will render an with a null value by default resulting in an "empty" page.". Can someone please help me with this issue
import './App.css';
import Home from './components/Home';
import AddRecipe from './components/AddRecipe';
import items from './data';
import React, { useState } from 'react';
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom';
const App = () => {
const [itemsList, setItemsList] = useState(items)
const addRecipe = (recipeToAdd) => {
setItemsList(itemsList.concat([recipeToAdd]));
}
const removeItem = (itemToRemove) => {
setItemsList(itemsList.filter(a => a!== itemToRemove))
}
return (
<Router>
<Routes>
<Route path="/addRecipe" render={ ({history}) => {
return (<AddRecipe onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
history.push('/');
} } />);
} } />
</Routes>
</Router>
);
}
export default App;
In react-router-dom version 6, you should use element prop for this.
I suggest your read their document on upgrading from version 5 where they explain the changes.
For your problem, you should write something like this:
<Route
path="/addRecipe"
element={
<AddRecipe
onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
history.push('/');
}
/>
}
/>
The Route component API changed significantly from version 5 to version 6, instead of component and render props there is a singular element prop that is passed a JSX literal instead of a reference to a React component (via component) or a function (via render).
There is also no longer route props (history, location, and match) and they are accessible only via the React hooks. On top of this RRDv6 also no longer surfaces the history object directly, instead abstracting it behind a navigate function, accessible via the useNavigate hook. If the AddRecipe component is a function component it should just access navigate directly from the hook. If it unable to do so then the solution is to create a wrapper component that can, and then render the AddRecipe component with the corrected onAddRecipe callback.
Example:
const AddRecipeWrapper = ({ addRecipe }) => {
const navigate = useNavigate();
return (
<AddRecipe
onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
navigate('/');
}}
/>
);
};
...
const App = () => {
const [itemsList, setItemsList] = useState(items);
const addRecipe = (recipeToAdd) => {
setItemsList(itemsList.concat([recipeToAdd]));
};
const removeItem = (itemToRemove) => {
setItemsList(itemsList.filter(a => a !== itemToRemove))
};
return (
<Router>
<Routes>
<Route
path="/addRecipe"
element={<AddRecipeWrapper addRecipe={addRecipe} />}
/>
</Routes>
</Router>
);
};

React Router DOM listen for Route changes

My react app is inside a java struts project, which includes a header. There is a certain element in that header that changes depending on certain routes being hit.
For this it would be much simpler to listen to when a route changes where my Routes are defined. As opposed to doing it in every route.
Here is my
App.js
import {
BrowserRouter as Router,
Route,
useHistory,
useLocation,
Link
} from "react-router-dom";
const Nav = () => {
return (
<div>
<Link to="/">Page 1 </Link>
<Link to="/2">Page 2 </Link>
<Link to="/3">Page 3 </Link>
</div>
);
};
export default function App() {
const h = useHistory();
const l = useLocation();
const { listen } = useHistory();
useEffect(() => {
console.log("location change");
}, [l]);
useEffect(() => {
console.log("history change");
}, [h]);
h.listen(() => {
console.log("history listen");
});
listen((location) => {
console.log("listen change");
});
return (
<Router>
<Route path={"/"} component={Nav} />
<Route path={"/"} component={PageOne} exact />
<Route path={"/2"} component={PageTwo} exact />
<Route path={"/3"} component={PageThree} exact />
</Router>
);
}
None of the console logs get hit when clicking on the links in the Nav component. Is there a way around this?
I have a CodeSandbox to test this issue.
Keep in mind that react-router-dom passes the navigation object down the React tree.
You're trying to access history and location in your App component, but there's nothing "above" your App component to provide it a history or location.
If you instead put your useLocation and useHistory inside of PageOne/PageTwo/PageThree components, it works as intended.
Updated your codesandbox:
https://codesandbox.io/s/loving-lamarr-bzspg?fontsize=14&hidenavigation=1&theme=dark

How to correctly redirect user after authentication?

I am using react-redux and currently I have a bool isAuthenticated property in my global state and if this is true it will redirect to /home component.
Is there a better approach to the one I implemented? How can I resolve the output error?
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { login } from "./actions";
import { useHistory } from "react-router-dom"
const Login = () => {
const dispatch = useDispatch();
const history = useHistory();
const myState = useSelector(
state => state.authentication
);
const onSubmit = (values) => {
dispatch(login(values.email, values.password));
};
useEffect(() => {
if (myState.isAuthenticated) {
history.push("/home")
}
}, [isAuthenticated]);
}
Warning: React Hook useEffect has a missing dependency: 'history'. Either include it or remove the dependency array
[isAuthenticated, history] is this acceptable?
Is there a better approach to the one I implemented? How can I resolve the output error?
The answer depends on where you keep your logic. If you keep all your logic in components' states, the useEffect way is perfectly fine.
To resolve the warning, just put the history to the useEffect inputs as below:
useEffect(() => {
if (myState.isAuthenticated) {
history.push("/home")
}
}, [myState.isAuthenticated, history]);
Now the useEffect will be triggered every time the reference to the history object changes. If you're using React Router, then the history instance is most probably mutable and it will not change, and hence not trigger the useEffect. So it should be safe to include it in the UseEffect dependency array.
The better way is always create separate PrivateRoute component which handle your authentication. If you go through react router official documentation they have given a good example of authentication:
function PrivateRoute({ children, ...rest }) {
const myState = useSelector(
state => state.authentication
);
return (
<Route
{...rest}
render={({ location }) =>
myState.isAuthenticated ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
You can use wrap it in layer like this:
<Switch>
<Route path="/public">
<PublicPage />
</Route>
<Route path="/login">
<LoginPage />
</Route>
<PrivateRoute path="/protected">
<ProtectedPage />
</PrivateRoute>
</Switch>
In that way you dont have to manually track your authentication.
Here is complete demo from react-router website:https://reactrouter.com/web/example/auth-workflow

React D3 component did not get unmounted upon (component) redirect using history.push()

React D3 component did not get unmounted upon (component) redirect using the following approach. That is, in a SPA, while on 'graphA', clicking a button redirects to 'graphB'. 'graphB' is rendered, however, 'graphA' is still visible. Thoughts on how I can remove/unmount 'graphA' such that only 'graphB' is visible. I tried calling ReactDOM.unmountComponentAtNode() in various React lifecycle hooks with no success.
this.props.history.push({ pathname: '/graphB' })
Assuming you're using React Router, this is how to use a Switch component. It will render the component for the first path that matches.
import React, { Component } from 'react';
import { render } from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
const A = () => <h1>A</h1>
const B = () => <h1>B</h1>
const Landing = ({ history }) => {
const goToA = () => {
history.push('a');
}
const goToB = () => {
history.push('b');
}
return (
<div>
<button onClick={goToA}>Go to A</button>
<button onClick={goToB}>Go to B</button>
</div>
)
}
const App = ({ history }) => {
return (
<Switch>
<Route exact path="/" component={Landing} />
<Route path="/a" component={A} />
<Route path="/a" component={A} />
</Switch>
)
}
render(
<BrowserRouter>
<App />
</BrowserRouter>
, document.getElementById('root'));
Live example here.

Categories