React-router : conditionally redirect at render time - reactjs

So I have this basic component <Redirectable />:
import React from 'react';
import {
useParams,
useHistory,
Redirect,
} from 'react-router-dom';
export default () => {
const history = useHistory();
const {id} = useParams();
if (!checkMyId(id) {
// invalid ID, go back home
history.push('/');
}
return <p>Hey {id}</p>
}
But I get the following error:
Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
I also tried: <Redirect push to="/" />, but same error.
What's the correct way to handle this? I read about onEnter callback at <Router /> level, but as far as I'm concerned, the check should happen at <Redirectable /> level.
There should be a solution, shouldn't it? I don't feel like I'm doing something completely anti-react-pattern, am I?

This seems to do the trick. I was not able to find any documentation as to why this occures. All I was able to find was different examples with callbacks but this solved it for me.
import React from 'react';
import {
useParams,
useHistory,
Redirect,
} from 'react-router-dom';
const MyComponent = () => {
const history = useHistory();
const {id} = useParams();
if (!checkMyId(id) {
// invalid ID, go back home
history.push('/');
}
return <p>Hey {id}</p>
}
export default MyComponent;
It seems that react may recognize export default () => { as a pure component and so side effects are prohibited.

Yes, it seems to me you are written the component in a anti pattern way. Can you please update like below:
const Rediractabke = () => {
const history = useHistory();
const {id} = useParams();
if (!checkMyId(id) {
// invalid ID, go back home
history.push('/');
}
return <p>Hey {id}</p>
}
export default as Redirectable;

#c0m1t was right, the solution was to use useEffect:
import React, {useEffect} from 'react';
import {
useParams,
useHistory,
Redirect,
} from 'react-router-dom';
export default () => {
const history = useHistory();
const {id} = useParams();
useEffect(() => {
if (!checkMyId(id) {
// invalid ID, go back home
history.push('/');
}
})
return <p>Hey {id}</p>
}

Related

React - Warning: Cannot update a component (`PrivateRoute`) while rendering a different component (`Modules`)

Getting the following error on all child components.
react-dom.development.js:86 Warning: Cannot update a component
(PrivateRoute) while rendering a different component (Example). To
locate the bad setState() call inside Examples,
I've found lots of examples of the same error but thus far no solutions
React Route Warning: Cannot update a component (`App`) while rendering a different component (`Context.Consumer`)
Can Redux cause the React warning "Cannot update a component while rendering a different component"
The PrivateRoute wraps the component to redirect if not logged in.
export default function PrivateRoute() {
const session: ISessionReducer = useSelector((state: RootState) => state.core.session);
useEffect(() => {
if (!session.jwt) <Navigate to="/login" />;
}, [session]);
return <Outlet />;
};
It is happening because useEffect runs after the component is rendered. So what's happening in this case is that your Outlet component is getting rendered first before your code in useEffect runs. So if the jwt token doesn't exist then it will try to redirect but it won't be able to because your Outlet will already be rendered by then.
So I can give you the solution of what I use to check if the jwt token exist.
1.) I create a custom hook for checking if the token exists.
2.) And then I use that custom hook in my privateRoute component to check if the user is loggedIn.
useAuthStatus.js
import { useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
export const useAuthStatus = () => {
const [loggedIn, setLoggedIn] = useState(false)
const [checkingStatus, setCheckingStatus] = useState(true)
const { user } = useSelector((state) => state.auth)
useEffect(() => {
if (user?.token) {
setLoggedIn(true)
} else {
setLoggedIn(false)
}
setCheckingStatus(false)
}, [user?.token])
return { loggedIn, checkingStatus }
}
PrivateRoute component
import { Navigate, Outlet } from 'react-router-dom'
import { useAuthStatus } from '../../hooks/useAuthStatus'
import CircularProgress from '#mui/material/CircularProgress'
const PrivateRoute = () => {
const { loggedIn, checkingStatus } = useAuthStatus()
if (checkingStatus) {
return <CircularProgress className='app__modal-loader' />
}
return loggedIn ? <Outlet /> : <Navigate to='/login' />
}
export default PrivateRoute

mapStateToProps react router dom v6 useParams()

BlogDetailsPage.js
import { connect } from "react-redux";
import { useParams } from "react-router-dom";
const BlogDetailsPage = (props) => {
const { id } = useParams();
return <div>Blog Details: {}</div>;
};
const mapStateToProps = (state, props) => {
const { id } = useParams();
return {
blog: state.blogs.find((blog) => {
return blog.id === id;
}),
};
};
export default connect(mapStateToProps)(BlogDetailsPage);
How to use mapStateToProps in "useParams()" react-router-dom ?
and whatever links that navigate to /slug path are ended up in BlogDetailsPage.js, Since BlogDetailsPage.js is being nested nowhere else so i couldn't get specific props pass down but route params. From my perspective this is completely wrong but i couldn't figure out a better way to do it.
Compiled with problems:X
ERROR
src\components\BlogDetailsPage.js
Line 11:18: React Hook "useParams" is called in function "mapStateToProps" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use" react-hooks/rules-of-hooks
Search for the keywords to learn more about each error.```
Issue
React hooks can only be called from React function components or custom React hooks. Here it is being called in a regular Javascript function that is neither a React component or custom hook.
Solutions
Preferred
The preferred method would be to use the React hooks directly in the component. Instead of using the connect Higher Order Component use the useSelector hook to select/access the state.blogs array.
Example:
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
const BlogDetailsPage = () => {
const { id } = useParams();
const blog = useSelector(state => state.blogs.find(
blog => String(blog.id) === id
));
return <div>Blog Details: {}</div>;
};
export default BlogDetailsPage;
Alternative/Legacy
If you have the need to access path params in any mapStateToProps function, if you are using a lot of oder code for example, then you'll need to create another HOC to access the path params and have them injected as props so they are available in the mapStateToProps function.
Example:
import { useParams, /* other hooks */ } from "react-router-dom";
const withRouter = Component => props => {
const params = useParams();
// other hooks, useLocation, useNavigate, etc..
return <Component {...props} {...{ params, /* other injected props */ }} />;
};
export default withRouter;
...
import { compose } from 'redux';
import { connect } from 'react-redux';
import withRouter from '../path/to/withRouter';
const BlogDetailsPage = ({ blog }) => {
return <div>Blog Details: {}</div>;
};
const mapStateToProps = (state, { params }) => {
const { id } = params || {};
return {
blog: state.blogs.find((blog) => {
return String(blog.id) === id;
}),
};
};
export default compose(
withRouter, // <-- injects a params prop
connect(mapStateToProps) // <-- props.params accessible
)(BlogDetailsPage);
I think, react hook functions are allowed to use inside of react component.
Outside of react components, it's not allowed to use react api hook functions.
Thanks, I'd liked to help you my answer.

this.props.history.push is undefined

I am creating a form with some predefined values, and i want to route to the dashboard page once the form is submitted. I am using handleLoginSubmit func on the onsubmit of the form. For that, I have the following code:
handleLoginSubmit = (e) => {
e.preventDefault();
let hardcodedCred = {
email: "email#email.com",
password: "password123",
};
if (
this.state.email == hardcodedCred.email &&
this.state.password == hardcodedCred.password
) {
//combination is good. Log them in.
//this token can be anything. You can use random.org to generate a random string;
// const token = "123456abcdef";
// sessionStorage.setItem("auth-token", token);
//go to www.website.com/todo
// history.push("/dashboard");
this.props.history.push("/dashboard");
// console.log(this.state.route);
console.log(this.context);
console.log("logged in");
// <Link to={location} />;
} else {
//bad combination
alert("wrong email or password combination");
}
};
But, I am receiving the error saying that history is undefined.
Please help me out here
You need to export the component with router to access history
import { withRouter } from 'react-router-dom';
export default withRouter(ComponentName)
if your component is linked with Route then you can directly access that in this.props but if its sub-component or children of some component then you cant access it into this.props.
so there is multiple way to solve it
pass history as props in your component if that component is linked with Route
<Component history={this.props.history} />
use withRouter HOC, that bind all Route props in your component
import { withRouter } from 'react-router-dom';
export default withRouter(Component)
use useHistory hook and save it to constant ( functional component )
import { useHistory } from 'react-router-dom';
const history = useHistory();
Update of Nisharg Shah's answers of part 3 for future reference.
If you are using react-router-dom v6, then use useNavigate instead of useHistory.
You can use:
import { useNavigate } from "react-router-dom";
let navigate = useNavigate();
OR
import { useNavigate } from "react-router-dom";
function App() {
let navigate = useNavigate();
function handleClick() {
navigate("/home");
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}

How to use useParams in mapStateToProps?

I want specific prop from route params and use it to filter data in redux-store.
Product.js
import React from 'react';
import { useParams } from 'react-router-dom';
import { connect } from 'react-redux';
const Product = (props) => {
let { slug } = useParams();
//console.log(props.match)
return (
<div>
<h3>Welcome to <b>{ slug }</b> page</h3>
</div>
)
}
const mapStateToProps = ( state, ownProps ) => {
// let id = slug;
return { item: state.items[0]}
}
export default connect(
mapStateToProps
)(Product);
App.js
function App() {
return (
<Router>
<Navbar/>
<Switch>
<Route exact path="/:slug">
<Product/>
</Route>
<Route exact path="/">
<Home/>
</Route>
</Switch>
</Router>
);
}
and whatever links that navigate to /slug path are ended up in Product.js, Since Product.js is being nested nowhere else so i couldn't get specific props pass down but route params. From my perspective this is completely wrong but i couldn't figure out a better way to do it.
Since you are using the new version of React and Redux. You can try use Hook to get data from redux store.
Better call useSelector instead. Read more here
import React from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
const Product = () => {
let { slug } = useParams();
const item = useSelector((state) => state.items[slug]);
console.log(item);
return (
<div>
<h3>
Welcome to <b>{slug}</b> page
</h3>
</div>
);
};
export default Product;
In your case, you could use the mapDispatchToProps property which is the second argument of connect
Product.js
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { connect } from 'react-redux';
const Product = (props) => {
const { slug } = useParams();
const {
items, // From mapStateToProps
filterItems // From mapDispatchToProps
} = props;
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
setFilteredItems(filterItems(items, slug));
});
return (
<div>
<h3>Welcome to <b>{ slug }</b> page</h3>
<!-- {filteredItems.map(item => { })} -->
</div>
)
}
const mapStateToProps = ( state, ownProps ) => {
return { items: state.items}
}
const mapDispatchToProps = dispatch => {
return {
filterItems: (items, filter) => {
// TODO: Filter logic goes here...
return items;
}
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Product);
Another performant solution is to use withRouter
You want to access the state and routing params in one place to select what you want from the state.
One solution to it is using useParams inside the component and while you access the state using connect it's fine.
However, I once found that this solution causes my component to re-render a lot because we don't quietly control how useParams being re-invoked, I prefer using the HOC(higher order component) that react-router-dom offer which is called withRouter (which I found more performant) and here is how to use it
You wrap it around connect
import { withRouter } from "react-router-dom";
...
export const ArticlePageContainer = withRouter(
connect(mapStateToProps, undefined)(ArticlePageUI)
);
then you can access the slug or any params from inside the props in mapStateToProps function
function mapStateToProps(state, props) {
const slug = props.match.params.slug;
return {
targetArticle: state.items.find((item) => item.slug == slug)
};
}
Finally, you use that selected piece of data as in your component, where you get it from the props directly now.
function ArticlePageUI(props) {
return (
<>
<p>{"Article Page"}</p>
<p>{props.targetArticle?.content}</p>
</>
);
}
Here's a code sandbox where you can check the implementation yourself
https://codesandbox.io/s/stackoverflowhow-to-use-useparams-in-mapstatetoprops-qxxdo?file=/src/article-page.js:87-225

How to redirect to a new page from a function in React?

Right now I have this function in react and I am using it to go back to login and also to check reset the localStorage value for which I am using the function and not since using that I cannot reset local storage value. The function is below:-
logout(){
localStorage.clear();
console.log("cliasdk");
return(
<Redirect to="/login"/>
)
}
This gets executed on clicking a div but I am not able to go to the /login page.How to do it?
If you use the react-router-dom package you can wrap your component with a router and then you have the ability to redirect the user programmatically, like so this.props.history.push('/login').
Eg:
import {withRouter} from 'react-router-dom';
class Component extends React.component {
constructor(props){
}
componentDidMount(){
this.props.history.push('/login');
}
}
export default withRouter(Component);
See: https://www.npmjs.com/package/react-router-dom.
With all previous answers, I'll describe here this use case:
on `/login` page, I would like to go to `/` when login is OK:
Add imports:
import { Redirect } from 'react-router-dom';
Add in your component default state a redirect to false:
state = {
redirect: false,
}
Add to your business logic (ex. onLoginOk()) a change of the redirect state
this.setState({ redirect: true })
Add somewhere in your render root element:
{ this.state.redirect ? (<Redirect push to="/"/>) : null }
That's it.
you can use this example for redirect after rendering a function
import React from 'react';
import { Redirect } from 'react-router-dom';
class MyComponent extends React.Component {
state = {
redirect: false
}
setRedirect = () => {
this.setState({
redirect: true
})
}
renderRedirect = () => {
if (this.state.redirect) {
return <Redirect to='/target' />
}
}
render () {
return (
<div>
{this.renderRedirect()}
<button onClick={this.setRedirect}>Redirect</button>
</div>
)
}
}
You can use history variable in props or if haven't history in props, you can use withRouter HOC (https://reacttraining.com/react-router/web/api/withRouter)
history.push("/login")
or
history.replace("/login")
This is the simplest if you don't want to deal with react-router-dom.
Here's an example written in react functional components
const Page = () => {
const redirect = () => {
window.location.href = '/anotherPagePath'
}
return (
<button onClick={redirect}>go to another page</button>
)
}
import React from "react"
import { useHistory } from "react-router-dom";
export const Component = ( props ) => {
const history = useHistory()
const handler = () => {
//Redirect to another route
history.push("/route-link")
}
}
Maybe that's what you are looking for.
If you are trying to logout in a React application (that uses the URL pattern /#/page) through a function that clean the local storage / redirect, try to use go:
import { createHashHistory } from "history";
const history = createHashHistory();
history.go("/login");
The go loads a specific URL from the history list, this can be used to go back in the history, but also to go foward, in this case the 'foward' will use /login and the absolute path to redirect.
Update
On React Router 6 you can use useNavigate to navigate programmatically
In React router 6, redirection looks like this:
const navigate = useNavigate();
const goToLoginPage = () => navigate('/login');
All the code can be seen here:
https://github.com/anmk/redirect/blob/redirect/src/App.js
You can also write a component to do this:
https://github.com/anmk/redirect/tree/redirect_to_component
You can change route programmatically, with history like this:
export default class Logout extends Component {
logout = () => {
this.props.history.push("login");
};
render() {
return (
<div>
<h1>Logout</h1>
<button onClick={this.logout}>Logout</button>
</div>
);
}
}
If you need localStorage.clear();, just put it in logout function. You can see full (working) code example here: https://codesandbox.io/s/py8w777kxj
For future reference, if you're not interested in using React Router you could try the same as I use right now which uses the browser's location (URL):
logout(){
// stuff...
location.href = "/login/"
}
Try this
import React from "react";
const createHistory = require("history").createBrowserHistory;
class Logout extends React.Component {
constructor(props) {
super(props);
let history = createHistory();
history.push("/login");
let pathUrl = window.location.href;
window.location.href = pathUrl;
}
render() {
return (
<div>
</div>
);
}
}
export default Logout;
logout(){
localStorage.clear();
this.setState({redirect: true})
}
//inside Render
render(){
const {redirect} = this.state;
if(redirect){
return <Redirect push to="/login"/>
}
}
You need to import Redirect from react-router-dom, like this:
import { Redirect } from 'react-router-dom';
This is how I solved the problem.
import {useDispatch} from "react-redux";
import useRouter from 'hooks/useRouter'
const {push} = useRouter();
const dispatch = useDispatch();
const logout = () => {
localStorage.clear();
push("/login");
dispatch({
type: LOGOUT_STARTED,
payload: false,
});
};
<... onClick={logout} ..>

Resources