React Router (v4) not redirecting in componentDidUpdate() - reactjs

I'm trying to trigger a redirect if a user is logged in. A successful login triggers an update of this.state.user so I'd like to handle the redirect in componentDidUpdate() or another lifecycle method.
The if statement is getting called when I intend for it to, but the redirect does nothing. Any idea as to how I can fix this? I just want this to update the url so it doesn't necessarily need to use Redirect.
I'm not using user authentication currently and don't intend to add it yet.
import React, { Component } from "react";
import "./App.css";
import { BrowserRouter as Router, Route, Redirect } from "react-router-dom";
import AuthContainer from "./components/AuthContainer";
import ChatSelector from "./components/ChatSelector";
import { debug } from "util";
// import ChatRoomContainer from './components/ChatRoomContainer';
class App extends Component {
constructor(props) {
super(props);
this.state = {
user: {}
};
}
setUser = user => {
console.log("setting user");
this.setState({ user });
};
componentDidUpdate() {
// if a user is logged in, redirect them to chat-selector
if (Object.keys(this.state.user).length > 0) {
console.log(this.state.user);
<Router>
<Redirect to="/chat-selector" />;
</Router>;
}
}
render() {
return (
<Router>
<div>
<Route
exact
path="/"
render={props => (
<AuthContainer {...props} setUser={this.setUser} />
)}
/>
<Route
exact
path="/chat-selector"
render={props => <ChatSelector {...props} user={this.state.user} />}
/>
{/* <Route exact path='/chatroom' component={ChatRoomContainer}/> */}
</div>
</Router>
);
}
}
export default App;

I solved this by placing the if statement within render, and adding a redirect boolean to state.
import React, { Component } from "react";
import "./App.css";
import {
BrowserRouter as Router,
Route,
Redirect,
withRouter
} from "react-router-dom";
import AuthContainer from "./components/AuthContainer";
import ChatSelector from "./components/ChatSelector";
import { debug } from "util";
// import ChatRoomContainer from './components/ChatRoomContainer';
class App extends Component {
constructor(props) {
super(props);
this.state = {
user: {},
redirect: false
};
}
setUser = user => {
console.log("setting user");
this.setState({ user });
};
redirect = () => {
this.setState({ redirect: true });
};
render() {
if (
Object.keys(this.state.user).length > 0 &&
this.state.redirect === true
) {
this.setState({ redirect: false });
console.log("logged in");
return (
<Router>
<Redirect to="/chat-selector" />
</Router>
);
} else {
console.log("not logged in");
}
return (
<Router>
<div>
<Route
exact
path="/"
render={props => (
<AuthContainer
{...props}
setUser={this.setUser}
redirect={this.redirect}
/>
)}
/>
<Route
exact
path="/chat-selector"
render={props => <ChatSelector {...props} user={this.state.user} />}
/>
{/* <Route exact path='/chatroom' component={ChatRoomContainer}/> */}
</div>
</Router>
);
}
}
export default App;

There is actually a better way of doing this, and I have recently stumbled across a similar situation.
Since the <Redirect /> technique does not work well with helper functions or lifecycle methods, I suggest to instead use this.props.history.push() inside the ComponentDidUpdate() to perform a redirect. Just remember to wrap your component with the withRouter() HOC.
Example code here: http://blog.jamesattard.com/2018/03/fire-action-creator-after-react-state.html

Related

Using componentDidMount() to check user log in status on app load

I am running a React frontend app that checks whether a user is currently logged in on a Rails API. I used componentDidMount to check then set the state accordingly but when the page loads for the first time, and the user is logged in, the required info flashes on the page then the app renders and the state goes back to initial state.
Here is my code:
import React, { Component } from "react";
import { Routes, Route } from "react-router-dom";
import Dashboard from "./Dashboard";
import Home from "./Home";
import Header from "./Header";
import Signin from "./Signin";
import Signup from "./Signup";
import { withRouter } from "../withRouter";
import axios from "axios";
class App extends Component {
constructor(props) {
super(props);
this.state = {
loggedInStatus: "NOT_LOGGED_IN",
user: {},
};
this.handleLogin = this.handleLogin.bind(this);
}
checkLoginStatus() {
axios
.get("http://localhost:3001/logged_in", { withCredentials: true })
.then((response) => {
if (
response.data.logged_in &&
this.state.loggedInStatus === "NOT_LOGGED_IN"
) {
this.setState({
loggedInStatus: "LOGGED_IN",
user: response.data.user,
});
} else {
this.setState({
loggedInStatus: "NOT_LOGGED_IN",
user: {},
});
}
})
.catch((error) => console.log("check login error", error));
}
componentDidMount() {
this.checkLoginStatus();
}
handleLogin(data) {
if (data.logged_in) {
this.setState({
loggedInStatus: "LOGGED_IN",
user: data.user,
});
this.props.navigate("/");
}
}
render() {
return (
<React.StrictMode>
<Header />
<div className="app container mt-3">
<Routes>
<Route path="/" element={<Home user={this.state.user.email} />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route
path="/signin"
element={<Signin handleLogin={this.handleLogin} />}
/>
<Route
path="/signup"
element={<Signup handleLogin={this.handleLogin} />}
/>
</Routes>
</div>
</React.StrictMode>
);
}
}
export default withRouter(App);
Strict mode can't automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions: Class component constructor , render , and shouldComponentUpdate methods.
Try to remove strict mode and see if the behavior is the same.

Failed to compile: 'props' is not defined no-undef in ReactJS

I'm trying to pass a function to a a React Router but it gives me an error despite several adjustments. I tried putting the function in the render(), added this before params props, but nothing seems to be working. How do you you pass a function to selective return between a Route and a Redirect tag?
import React, { Component } from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import DogList from './DogList';
import DogDetails from './DogDetails';
class Routes extends Component {
constructor(props) {
super(props);
this.getDog = this.getDog.bind(this);
}
getDog() {
let name = props.match.params.name;
let currDog = this.props.dogs.find(
dog => dog.name.toLowerCase() === name.toLowerCase()
);
if(currDog != undefined) {
return <DogDetails {...props} dog={currDog} /> ;
} else {
return <Redirect to="/dogs" />
}
}
render() {
return(
<Switch>
<Route exact path='/dogs' render= {() => <DogList dogs={this.props.dogs} />} />
<Route exact path='/dogs/:name' render={(props) => {this.getDog()}} />
<Redirect to='/dogs' />
</Switch>
);
}
}
export default Routes;
I recommend you to seperate your components because there might be so many routes, so, you might not be able to manage them in one component.
Anyway, in your case please try sending props as a parameter to your function.
You should wrap your switches with BrowserRouter.
import React, { Component } from 'react';
import { Switch, Route, Redirect, BrowserRouter as Router } from 'react-router-dom';
import DogList from './DogList';
import DogDetails from './DogDetails';
class Routes extends Component {
constructor(props) {
super(props);
this.getDog = this.getDog.bind(this);
}
getDog(props) {
const { dogs } = this.props;
let name = props.match.params.name;
let currDog = dogs.find(
dog => dog.name.toLowerCase() === name.toLowerCase()
);
if(currDog != undefined) {
return <DogDetails {...props} dog={currDog} /> ;
} else {
return <Redirect to="/dogs" />
}
}
render() {
const { dogs } = this.props;
return(
<Router>
<Switch>
<Route exact path='/dogs' render= {() => <DogList dogs={dogs} />} />
<Route exact path='/dogs/:name' render={(props) => this.getDog(props)} />
<Redirect to='/dogs' />
</Switch>
</Router>
);
}
}
Keep in mind this react router documentation. It is a good guide to your example; https://reactrouter.com/web/guides/quick-start
A complete example is here; https://codesandbox.io/s/sleepy-ishizaka-n0433?file=/src/App.js
Use this.props not only props
let name = this.props.match.params.name;

How to redirect to another route

There is one need for url authentication:
import React from "react";
import { connect } from "react-redux";
import { Switch, Route, Redirect } from "react-router-dom";
...
const IndexRouter = ({ loggedIn }) => (
<Switch>
<Route
path="/"
render={() => (loggedIn ? <Redirect to="/dashboard" /> : <Login />)}
/>
<Route exact path="/dashboard" component={DashboardRouter} />
<Route exact path="/stock" component={StockRouter} />
</Switch>
);
export default connect(
state => ({
loggedIn: state.persist.loggedIn
}),
{}
)(IndexRouter);
The code means if I have not logged in, all of url are required from client will redirect to Login component. Other than that it will route to DashboardRouter.
The StockRouter is another route related with DashboardRouter.
The problem is that if I logged in. All the unspecific url (except /dashboard, /stock) I manually typed showing the /dashboard url without anything. The specific url such as /stock can show the component StockRouter directly.
You would need to write a PrivateRoute wrapper around your Route and change the order of Routes in IndexRouter, so that the Route with path / is matched at the last otherwise all routes will match / first and will not render correctly
const PrivateRoute = ({component: Component, loggedIn, ...rest }) => {
if(!loggedIn) {
return <Redirect to="/login" />
}
return <Route {...rest} component={Component}/>
}
}
}
const IndexRouter = ({ loggedIn }) => (
<Switch>
<PrivateRoute exact path="/dashboard" component={DashboardRouter} />
<PrivateRoute exact path="/stock" component={StockRouter} />
<Redirect to="/dashboard" />
</Switch>
);
For more details, check Performing Authentication on Routes with react-router-v4
Just create a history component like this :
import React from "react";
import {withRouter} from "react-router";
let globalHistory = null;
class HistoryComponent extends React.Component {
componentWillMount() {
const {history} = this.props;
globalHistory = history;
}
componentWillReceiveProps(nextProps) {
globalHistory = nextProps.history;
}
render() {
return null;
}
}
export const GlobalHistory = withRouter(HistoryComponent);
export default function gotoRoute(route) {
return globalHistory.push(route);
}
And then import into your component:
import gotoRoute from "../../history";
gotoRoute({
pathname: "/your_url_here",
state: {
id: this.state.id
}
});
And in index.js
import {GlobalHistory} from "./history";
ReactDOM.render((
<Provider store={store}>
<BrowserRouter >
<div>
<GlobalHistory/>
<App/>
</div>
</BrowserRouter>
</Provider>
), document.getElementById('root'));

ComponentDidMount called multiple times

I built a HOC to use on protected routes in my app. It takes in the component that should be rendered at the route, checks if the user is authenticated, and then renders that component if they are. It works, but it causes the component to mount/unmount several times (as many times as the render function in my app.js file is called).
routes from my app.js
<Switch>
<Route path='/groups/show/:id'
component={ RequireAuth(Group) } />
<Route path='/groups/create'
component={ RequireAuth(CreateGroup) } />
<Route path='/groups'
component={ RequireAuth(GroupsMenu) } />
<Route path='/tutorials/:id' component={ Tutorial } />
<Route path='/tutorials' component={ TutorialMenu } />
<Route path='/ranked' component={ RankedPlay } />
<Route path='/casual' component={ CasualPlay } />
<Route path='/offline' component={ OfflinePlay } />
<Route path='/signup' component={ Signup } />
<Route path='/' component={ Menu } />
</Switch>
require_auth.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { store } from '../../index';
import { AUTH_ERROR } from '../../actions';
import PropTypes from 'prop-types';
import Display from './display';
export default function(ComposedComponent) {
class Authentication extends Component {
static propTypes = {
history: PropTypes.object.isRequired
};
componentWillMount() {
const { history } = this.props;
const error = 'You must be logged in to do this. Please login';
if (!this.props.authenticated) {
store.dispatch({ type: AUTH_ERROR, payload: error });
history.push('/');
}
}
componentWillUpdate(nextProps) {
const { history } = this.props;
const error = 'You must be logged in to do this. Please login';
if (!nextProps.authenticated) {
store.dispatch({ type: AUTH_ERROR, payload: error });
history.push('/');
}
}
render() {
return (
<Display if={ this.props.authenticated } >
<ComposedComponent { ...this.props } />
</Display>
);
}
}
function mapStateToProps(state) {
return {
authenticated: state.auth.authenticated
};
}
return withRouter(connect(mapStateToProps)(Authentication));
}
If you remove RequireAuth() from any of the routes, the component only mounts once when you hit the route. But adding it causes the component to mount every time app.js render() fires. Is there a way I can set this up so the component only mounts once?
By calling RequireAuth(Component) in render, you are decorating Component with your HOC in every render call, making that each render returns a new Component each render.
You should decorate Group, CreateGroup and GroupsMenu with RequireAuth, before exporting them. Just as you would with react-redux's connect.

React router v4 wait for xhr authentication to transition to route

I am trying to implement some server side authentication (via xhr) while using React Router v4. I do not want the route to transition until I validate with my server that a user is authenticated (by having a token) as well as the token is stored in session storage (not that this needs to be async).
Currently the issue is that my "private" route is still trying to render even though the user is not authenticated.
My React Router routes look like:
class AppContainer extends Component {
render() {
return (
<div>
<main>
<Switch>
<Route exact path='/' component={Home} />
<PrivateRoute path='/dashboard' component={Dashboard} />
</Switch>
</main>
</div>
);
}
}
PrivateRoute, as specified looks like:
const isAuthenticated = async () => {
const resp = await axios.get('http://localhost/api/session');
const token = _.get(resp, 'data.success');
const authObj = storage.getFromSession('TOKEN');
return !_.isNil(_.get(authObj, 'Token')) && token;
};
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
isAuthenticated() ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/',
state: { from: props.location }
}}/>
)
)}/>
)
export default PrivateRoute;
The Dashboard is trying to render even though the user is not authenticated. How would I wait for my api call to be returned and then redirect the user to either /dashboard or / (home page)?
My last try you can use a component like this:
import React, {PropTypes} from 'react';
import {Redirect} from 'react-router-dom';
export default class PrivateRoute extends React.Component {
constructor(props) {
super(props);
this.state={loading:true,authenticated:false}
}
componentDidMount(){
/* your authentication logic...*/
setTimeout(()=>{
this.setState({loading:false,authenticated:true});
},3000)
}
render() {
if(this.state.loading)
return <h1>Loading</h1>;
if(this.state.authenticated)
return (this.props.children);
else
return <Redirect to="/" />
}
}
And use it in your router like this:
<Route path="/your-protected-route" component={()=><PrivateRoute><YourComponent /></PrivateRoute>} />

Resources