ComponentDidMount called multiple times - reactjs

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.

Related

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;

Redux Authentication not using updated props?

privateRoute.js
import React, { Component } from "react";
import { BrowserRouter, Route, Switch, Redirect, withRouter} from "react-router-dom";
import { connect } from "react-redux";
function AuthRoute({ component: Component, isAuth, ...rest }) {
console.log(isAuth);
let test = false;
return (
<Route
{...rest}
render={props =>
isAuth.isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
}
const mapStateToProps = state => {
{
console.log(state);
return { isAuth: state.auth.isAuthenticated };
}
};
export default withRouter(connect(mapStateToProps, { pure: false })(AuthRoute));
App.js
import React, { Component } from "react";
import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";
import { library } from "#fortawesome/fontawesome-svg-core";
import PropTypes from "prop-types";
import {
faHome,
faClock,
faTasks,
faStickyNote,
faCalendarWeek
} from "#fortawesome/free-solid-svg-icons";
import { connect } from "react-redux";
import { loadUser, checkAuth } from "./actions/authActions";
import store from "./store";
import Home from "./Home";
import SideNav from "./Components/SideNav";
import Recent from "./Components/Recent";
import TopBar from "./Components/TopBar";
import AddNote from "./AddNote";
import LogIn from "./Components/LogIn/LogIn.js";
import Register from "./Components/Register/Register";
import ToDo from "./Components/ToDo/ToDo";
import AuthRoute from "./privateRoute";
library.add(faHome, faClock, faTasks, faStickyNote, faCalendarWeek);
class App extends Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false
};
}
static propTypes = {
isAuthenticated: PropTypes.bool
};
componentDidMount() {
store.dispatch(loadUser());
// this.props.isAuthenticated();
}
LogInContainer = () => {
return <Route path="/login" component={LogIn} />;
};
RegisterContainer = () => {
return <Route path="/register" component={Register} />;
};
DefaultContainer = () => {
return (
<div className="app_container">
<SideNav />
<TopBar />
<Route exact path="/" component={Home} />
<Route path="/recent" component={Recent} />
<Route path="/AddNote" component={AddNote} />
<Route path="/ToDo" component={ToDo} />
</div>
);
};
render() {
return (
<div>
<h1> {this.props.auth.isAuthenticated.toString()}</h1>
<BrowserRouter>
<Switch>
<Route exact path="/login" component={this.LogInContainer} />
<Route exact path="/register" component={this.RegisterContainer} />
<AuthRoute component={this.DefaultContainer} />
</Switch>
</BrowserRouter>
</div>
);
}
}
const mapStateToProps = state => {
return { auth: state.auth };
};
export default connect(mapStateToProps)(App);
When the user logins, an action will be dispatched to authentication the user. In redux dev tools, I can see the user authenticated.
Within privateRoute.js, mapStateToProps is always matching the initial state and not the updated state so isAuth never equates to true and I can't show the protected route?
I've been stuck on this for days, I feel like I'm implementing this wrong however looking at other examples I can't see what I am doing wrong.
The issue is how you are mapping state to props. It looks like your store state is structured as { auth: { isAuthenticated: boolean } }. However in the private route map state to props, you are trying to access effectively auth.isAuthenticated.isAuthenticated which will always be false/undefined. You need to go level higher in your state structure when mapping state to props. Try updating the private route component to:
import React, { Component } from "react";
import { BrowserRouter, Route, Switch, Redirect, withRouter } from "react-router-dom";
import { connect } from "react-redux";
function AuthRoute({ component: Component, isAuth, ...rest }) {
console.log(isAuth);
let test = false;
return (
<Route
{...rest}
render={props =>
isAuth.isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
}
const mapStateToProps = state => {
{
console.log(state);
return { isAuth: state.auth }; // change this line
}
};
export default withRouter(connect(mapStateToProps)(AuthRoute));

Can we redirect to in reduxSaga

I've a login page with HOC I pass component which must render be after successful login.
Here, if I check for isLoggedIn and redirect in render of sigin-in for then I get error
err: Invariant Violation: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops
saga.js
try{//call api
//put login success
<Redirect to="/app"/>
}
index.js
const AuthProfile=requireAuth(App);
In reactdom
<Route render={(props)=><AuthProfile {...props}/>} path="/app"/>
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import { bindActionCreators } from 'redux';
export default function (ComposedComponent) {
class Authenticate extends React.Component {
componentDidMount() {
console.log("12ra")
this._checkAndRedirect();
}
componentDidUpdate() {
this._checkAndRedirect();
}
_checkAndRedirect() {
const { isLoggedIn, redirect } = this.props;
if (!isLoggedIn) {
redirect();
}
}
render() {
console.log("28ra")
return (
<div>
{ this.props.isLoggedIn ? <ComposedComponent {...this.props} /> : null }
</div>
);
}
}
const mapStateToProps = (state) => {
return {
isLoggedIn:state.signInReducer.isLoggedIn
};
};
const mapDispatchToProps = dispatch => bindActionCreators({
redirect: () => push('/')
}, dispatch)
//Authenticate.propTypes = propTypes
return connect(
mapStateToProps,
mapDispatchToProps
)(Authenticate);
}
Is HOC component correct or I'm missing on something?
Is it good practise to redirect in saga?
Can anyone please lemme know how do I get to app component after success i'm stuck there please help thanks
UPDATE
saga.js
yield put({type:LOGIN_SUCCESS,payload:data})
yield put(push('/app'))
index.js
Ex for Page1,Page2 I need
<AuthRoute
exact
path="/"
component={Page1}
redirectTo="/login"
authenticated={this.props.isLoggegIn}
/>
<AuthRoute
exact
path="/"
component={Page2}
redirectTo="/login"
authenticated={this.props.isLoggegIn}
/>
Following is the way where you can navigate from saga:
saga.js
import { push } from 'react-router-redux';
...
// after API call & validate user
yield put(push('/app'));
index.js
<Route strict exact path='/app' component={App}>
<AuthRoute
exact
path="/"
component={Yourcomponent}
redirectTo="/login"
authenticated={hasAccessToken()}
/>
I am going to tell you the best and the simplest way from which you can redirect in any part of the application(i.e. inside redux and outside as well).
Step: 1
Create history.js file.
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
Step : 2 Import it inside App.js file (or, where you have made routers of your application).
import React from 'react';
import { Router, Route, Switch } from 'react-router-dom';
import * as ROUTES from '../../constants/Routes';
import history from './history';
const App = () => {
return (<Router history={history}>
<div className='App'>
<Switch>
<Route exact path={ROUTES.NOTIFICATIONS} component={Notification} />
<Route exacr path={ROUTES.USER_PROFILE} component={UserProfile} />
<Route exact path={ROUTES.SIGN_IN} component={SignInPage} />
<Route exact path={ROUTES.SIGN_UP} component={SignUpPage} />
<Route path='*' component={SignInPage} />
</Switch>
</div>
</Router>
);
};
export default App;
Step: 3 Inside Saga file just import the history.
import { push } from 'react-router-redux';
import { Auth } from "../service/api/Auth";
import history from '../containers/App/history';
import * as ACTION from '../constants/ActionType';
import { put, takeLatest, call } from "redux-saga/effects";
import { userLoginSuccess, userLoginFailed } from "../actions/auth";
export function* userLogin(loginData) {
const payLoad = loginData.payload;
try {
const loginUserData = yield call(Auth.userLoginApiCall, payLoad);
yield put(userLoginSuccess(TOKEN));
history.push('/posts'); // Redirect to Post Page
} catch (err) {
yield put(push(userLoginFailed()))
}
}
export default function* userLoginRequest() {
yield takeLatest(ACTION.USER_LOGIN_REQUEST, userLogin);
}

Infinite Loop With Higher Order React Redux Component

I am having some trouble with an infinite loop when using a Higher Order Component to handle authenticated routes. The authentication is very basic, and is just a boolean of true or false in the redux state.
When the app initially starts, it checks to see if there is a user stored in localStorage. If it does not return null, we immediately dispatch a SIGN_IN action.
import React from 'react'
import ReactDOM from 'react-dom'
import { HashRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import reducers from './reducers'
import reduxThunk from 'redux-thunk'
import { SIGN_IN } from './actions/types'
import logger from 'redux-logger'
const createStoreWithMiddleware = applyMiddleware(reduxThunk, logger)(createStore)
const store = createStoreWithMiddleware(reducers)
import App from './components/App'
const user = localStorage.getItem('user')
if(user) {
store.dispatch({ type: SIGN_IN })
}
ReactDOM.render (
<Provider store={store}>
<HashRouter>
<App />
</HashRouter>
</Provider>,
document.querySelector('.main')
)
This is the higher order component.
import React, { Component } from 'react';
import { connect } from 'react-redux';
export default function(ComposedComponent) {
class RequireAuth extends Component {
componentWillMount = () => {
if(!this.props.auth)
this.props.history.push('/')
}
render() {
return <ComposedComponent {...this.props} />
}
}
function mapStateToProps(state) {
return { auth: state.auth };
}
return connect(mapStateToProps)(RequireAuth);
}
The logic inside of componentWillMount seems to be what is causing the infinite loop, but I really do not know why. It seems to prevent me from going to any other routes, but as you can see in the console output below, it blows up in extraordinary fashion in doing so.
If it helps, here is the component, where I am wrapping my components with the HOC.
export default class App extends Component {
constructor(props) {
super(props)
}
render() {
return (
<div className="app"
style={{
backgroundImage: 'url("./img/base-bg.jpg")',
backgroundSize: 'cover',
backgroundPosition: 'center center',
backgroundRepeat: 'no-repeat'
}}
>
<Switch>
<Route path="/" exact component={RequireAuth(Intro)} />
<Route path="/landing" exact component={RequireAuth(Intro)} />
<Route path="/journey" component={RequireAuth(Exhibit)} />
<Redirect to="/landing" />
</Switch>
<Footer />
</div>
)
}
}
I am using React Router V4 if that helps. And for clarification, contains a login form, and a landing page. If the user is not authenticated, intro shows a login form, and when authed, the landing page should be shown. Perhaps this is where I am messing things up, and shouldn't be nesting my components. I am unsure though.
This boiled down to the hackalicious nesting and how I was wrapping my components with RequireAuth. Instead of having Login and Landing inside of a component called Intro, I scrapped that and changed my route config to this:
<Switch>
<Route path="/" exact component={Login} />
<Route path="/landing" exact component={RequireAuth(Landing)} />
</Switch>
Here is my HOC now, pretty much the same.
export default function(ComposedComponent) {
class RequireAuth extends Component {
componentDidMount = () => {
if(!this.props.auth)
this.redirectToLogin()
}
componentWillUpdate = nextProps => {
if(!nextProps.auth)
this.redirectToLogin()
}
redirectToLogin = () => this.props.history.push('/')
render() {
return <ComposedComponent {...this.props} />
}
}
function mapStateToProps(state) {
return { auth: state.auth };
}
return connect(mapStateToProps)(RequireAuth);
}

React Router (v4) not redirecting in componentDidUpdate()

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

Resources