I have a App component where user login into the application and the remaining components must verify auth guard before rendering, otherwise redirect to login page i.e. App component.
I am trying to pass the state variable from App component to the child components via a PrivateRouter as my auth guard. But its not working. Before this i have tried also using react-router v4 to use render inside the route.
App.js
import React, { Component } from 'react';
import axios from 'axios';
import history from './History';
import './App.css';
class App extends Component {
constructor(){
super();
this.state = {
username: '',
password: '',
authenticated: false
};
this.handleUserChange = this.handleUserChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event){
event.preventDefault();
axios.post('https://api.github.com/user',{}, {
auth: {
username: this.state.username,
password: this.state.password
}
}).then((response) => {
console.log(response.data);
this.setState({
authenticated : true,
});
history.push({pathname: '/home', state: { detail: response.data }});
history.go('/home');
}).catch(function(error) {
this.setState({
authenticated : false,
});
console.log('Error on Authentication' + error);
});
}
handleUserChange(event){
this.setState({
username : event.target.value,
});
}
handlePasswordChange = event => {
this.setState({
password: event.target.value
});
}
render() {
return (
<div className='loginForm'>
<form onSubmit={this.handleSubmit}>
<label>
username :
<input type="text" value={this.state.username} onChange={this.handleUserChange} required/>
</label>
<label>
password :
<input type="password" value={this.state.password} onChange={this.handlePasswordChange} required/>
</label>
<input type="submit" value="LogIn" />
</form>
</div>
);
}
}
export default App;
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import PrivateRoute from './PrivateRoute';
import {
Router,
Redirect,
Route,
Switch
} from "react-router-dom";
import Home from './Home';
import User from './User';
import history from './History';
ReactDOM.render(
<Router history={history}>
<Switch>
<Route path="/" exact component={App} />
<PrivateRoute path="/home" component={Home} />
<PrivateRoute path="/user" component={User} />
</Switch>
</Router>,
document.getElementById('root'));
registerServiceWorker();
Home.js
import React, { Component } from 'react';
import axios from 'axios';
import Autosuggest from 'react-autosuggest';
import './Home.css';
import history from './History';
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
const getSuggestionValue = suggestion => suggestion;
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion}
</div>
);
class Home extends Component {
constructor(props) {
super(props);
// Autosuggest is a controlled component.
// This means that you need to provide an input value
// and an onChange handler that updates this value (see below).
// Suggestions also need to be provided to the Autosuggest,
// and they are initially empty because the Autosuggest is closed.
this.state = {
value: '',
suggestions: [],
timeout: 0
};
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
console.log('=====++++ ' + newValue);
};
onSuggestionSelected = (event, { suggestion, suggestionValue, suggestionIndex, sectionIndex, method }) => {
console.log("Get the user +++++++++ " + suggestionValue);
if(suggestionValue && suggestionValue.length >= 1){
axios.get('https://api.github.com/users/'+ suggestionValue)
.then((response) => {
console.log("user selected : "+ response.data.avatar_url);
history.push({pathname: '/user', state: { detail: response.data }});
history.go('/user');
}).catch(function(error) {
console.log('Error on Authentication' + error);
});
}
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
if(this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.getSuggestions();
}, 500);
};
getSuggestions = () =>{
if(this.state.value && this.state.value.length >= 1){
axios.get('https://api.github.com/search/users',{
params: {
q: this.state.value,
in:'login',
type:'Users'
}
}).then((response) => {
console.log("users login : "+ response.data.items);
const userNames = response.data.items.map(item => item.login);
console.log("===== " + userNames);
this.setState({
suggestions: userNames
})
}).catch(function(error) {
console.log('Error on Authentication' + error);
});
}
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render(){
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: 'Type a userName',
value,
onChange: this.onChange
};
return (
<div>
<div>
Home page {this.props.location.state.detail.login}
</div>
<div>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
onSuggestionSelected={this.onSuggestionSelected}
/>
</div>
</div>
);
}
}
export default Home;
PrivateRouter.js
import React from 'react';
import {
Redirect,
Route,
} from "react-router-dom";
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
props.authenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/",
state: { from: props.location }
}}
/>
)
}
/>
);
export default PrivateRoute;
How can pass state variable authenticated to PrivateRouter or is there any better way of doing this?
Related
I have written below code,
1.I want to use Connect for storing usernamein local storage
2.I am using HOC component for logging purpose (callInfoLogger and callErrorLogger)
3.If I use connect and HOC together then this.props.history.push is not working (Its not redirecting to MyDashboard page)
Could you please let me know what do I need to do to fix the code?
App.js
import { BrowserRouter as Router, Route, Switch, } from "react-router-dom";
class App extends Component {
render() {
return (
<Router>
<Switch>
<Route path="/login" component={Login} />
<Route path="/dashboard" component={MyDashboard} />
</Switch>
</Router>
)
}
}
export default App;
Login.js
import React, { Component } from 'react';
import { withRouter } from "react-router-dom";
import { connect } from 'react-redux';
import HighLevelComponent from './HighLevelComponent';
class Login extends Component {
state = {
username: '',
password: '',
loginsuccess: true
}
callOnSubmit = (e) => {
e.preventDefault();
this.props.callErrorLogger("Inside call on Submit");
if (this.state.loginsuccess === true) {
this.props.callInfoLogger("Calling Info logger ");
this.props.onLoginSuccess(this.state.username);
this.props.history.push('/dashboard');
}
};
render() {
return (
<body>
<form className="login-form" onSubmit={this.callOnSubmit}>
<input
type="text" onChange={e => {
this.setState({
...this.state,
username: e.target.value
})
}}
/>
<input type="password"
onChange={e => {
this.setState({
...this.state,
password: e.target.value
})
}}
/>
<input type="submit" className="btnSbumit" value="LOG IN" />
</form>
</body>
)
}
}
const mapDispatchToProps = dispatch => {
return {
onLoginSuccess: (username) => dispatch({ type: "LOGIN_SUCCESS", username: username })
}
}
export default withRouter(HighLevelComponent(connect(null, mapDispatchToProps)(Login)));
MyDashboard.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
class MyDashboard extends Component {
render() {
return (
<body>
<h1>Welcome to React.. {this.props.username}</h1>
</body>
)
}
}
const mapStateToProps = state => {
return {
username: state.username
}
}
export default connect(mapStateToProps, null)(MyDashboard);
HighLevelComponent.js
import React from 'react';
const HighLevelComponent = (WrapperComponent) => {
class NewComponent extends React.Component {
callInfoLogger = (infomsg) => {
console.info(infomsg);
}
callErrorLogger = (errmsg) => {
console.error(errmsg);
}
render() {
return <WrapperComponent callInfoLogger={this.callInfoLogger} callErrorLogger={this.callErrorLogger} />
}
}
return NewComponent;
}
export default HighLevelComponent;
In the HOC names HighLevelComponent pass the props to the wrapper component as follows:
const HighLevelComponent = (WrapperComponent) => {
class NewComponent extends React.Component {
callInfoLogger = (infomsg) => {
console.info(infomsg);
}
callErrorLogger = (errmsg) => {
console.error(errmsg);
}
render() {
return <WrapperComponent callInfoLogger={this.callInfoLogger} callErrorLogger={this.callErrorLogger} {...props} />
}
}
return NewComponent;
}
Please note the {...props} on the wrapper component. In this way all the props will be further passed.
I'm new to React and I am setting up a small project. I am using a NodeJS server that answers to my request and I am trying to redirect the user after an successful login. I dispatch an action and update my redux store with the user information, that is working correctly. But when I try to redirect him I either get no errors and nothing happens or the URL changes but no component renders.
BTW in LoginForm.js I was trying to return a redirect after many fails by trying to add a callback with history object to my action.
So here is my code
App.js
import React, { Component } from 'react';
import LoginPage from './login/LoginPage';
import LandingPage from './landingpage/landing.page';
import ProtectedRoute from './protected/ProtectedRoute';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import PageNotFound from './common/pageNotFound';
class App extends Component {
render() {
return (
<Router >
<Switch>
<Route path="/login" component={() => <LoginPage />} />
<ProtectedRoute path="/" component={LandingPage} />
<Route component={() => <PageNotFound />} />
</Switch>
</Router>
)
}
}
export default App;
LoginPage.js
import React, { Component } from 'react'
import LoginForm from './LoginForm';
import PropTypes from 'prop-types'
import { connect } from 'react-redux';
import { login } from '../../actions/authActions';
import { withRouter } from "react-router";
class LoginPage extends Component {
render() {
const { login, userLoading, history } = this.props;
return (
<div>
<h1>Login in here</h1>
<LoginForm login={login} isLoading={userLoading} history={history} />
</div>
)
}
}
LoginPage.propTypes = {
login: PropTypes.func.isRequired
}
function mapStateToProps(state) {
return {
userLoading: state.auth.isLoading
}
}
export default connect(mapStateToProps, { login })(withRouter(LoginPage));
LoginForm.js
import React, { Component } from 'react'
import TextInput from '../common/TextInput';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
redirect: false,
}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({
[event.target.name]: event.target.value
})
}
handleSubmit(event) {
event.preventDefault();
this.setState({ error: null });
this.props.login(this.state);
this.setState({
redirect: true
})
}
render() {
const { isLoading, isAuth } = this.props;
const { redirect } = this.state;
console.log(redirect, isAuth)
if (redirect && isAuth) {
return <Redirect to="/" />
}
else {
return (
<form onSubmit={this.handleSubmit}>
<TextInput type="email" name="email" label="Email" onchange={this.handleChange} />
<TextInput type="password" name="password" label="Password" onchange={this.handleChange} />
{isLoading && <p>We are loggin you in</p>}
<button disabled={isLoading} type="submit">Log in</button>
</form>
)
}
}
}
const mapStateToProps = (state) => {
return {
isAuth: state.auth.isAuthenticated
}
}
LoginForm.propTypes = {
login: PropTypes.func.isRequired
}
export default connect(mapStateToProps)(LoginForm);
authActions.js
import {
LOGIN,
LOGIN_SUCCESS,
LOGIN_FAILED,
USER_LOADING,
USER_LOADED,
AUTH_ERROR,
REGISTER_SUCCESS,
REGISTER_FAIL,
} from '../constants';
export function login(payload) {
return dispatch => {
dispatch({ type: USER_LOADING })
setTimeout(function () {
return fetch('http://localhost:3000/user/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
}).then(
(res) => {
dispatch({ type: LOGIN_SUCCESS })
return res.json();
},
(err) => dispatch({ type: LOGIN_FAILED })
).then((data) => {
dispatch({
type: USER_LOADED,
payload: {
token: data.token,
userid: data.userID
}
})
});
}, 1000);
}
}
Since your LoginForm is wrapped with withRouter your can use this.props.history.pushState('/next-route')
I am trying to navigate to another page after a successful axios call which dispatch the action to change the state. But, I am sure that i wrongly call dispatch inside the axios call which result below error. I have tried many other ways like using ES6 arrow function to call the dispatch method but never works.
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.
App.js
import React, { Component } from 'react';
import axios from 'axios';
import history from './History';
import { Redirect } from "react-router-dom";
import './App.css';
import { connect } from 'react-redux';
import * as actionType from './reducer/action';
class App extends Component {
constructor(){
super();
this.state = {
username: '',
password: '',
loginData: []
};
this.handleUserChange = this.handleUserChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event){
event.preventDefault();
axios.post('https://api.github.com/user',{}, {
auth: {
username: this.state.username,
password: this.state.password
}
}).then((response) => {
console.log(response.data);
this.props.onLoginAuth(true);// This is where i am getting ERROR
//history.push({pathname: '/home', state: { detail: response.data }});
//history.go('/home');
this.setState({
loginData : response.data,
});
}).catch(function(error) {
console.log('Error on Authentication' + error);
});
}
handleUserChange(event){
this.setState({
username : event.target.value,
});
}
handlePasswordChange = event => {
this.setState({
password: event.target.value
});
}
render() {
if(this.props.authenticated){
console.log("Redirecting to Home page " + this.props.authenticated);
return <Redirect to={{ pathname: '/home', state: { detail: this.state.loginData } }}/>
}
return (
<div className='loginForm'>
<form onSubmit={this.handleSubmit}>
<label>
username :
<input type="text" value={this.state.username} onChange={this.handleUserChange} required/>
</label>
<label>
password :
<input type="password" value={this.state.password} onChange={this.handlePasswordChange} required/>
</label>
<input type="submit" value="LogIn" />
</form>
</div>
);
}
}
const mapStateToProps = state => {
return {
authenticated: state.authenticated
};
}
const mapDispatchToProps = dispatch => {
return {
onLoginAuth: (authenticated) => dispatch({type: actionType.AUTH_LOGIN, authenticated:authenticated})
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import PrivateRoute from './PrivateRoute';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import {
Router,
Redirect,
Route,
Switch
} from "react-router-dom";
import Home from './Home';
import User from './User';
import history from './History';
import reducer from './reducer/Login';
const store = createStore(reducer);
ReactDOM.render(
<Provider store= {store} >
<Router history={history}>
<Switch>
<Route path="/" exact component={App} />
<PrivateRoute path="/home" component={Home} />
<PrivateRoute path="/user" component={User} />
</Switch>
</Router>
</Provider>,
document.getElementById('root'));
registerServiceWorker();
Home.js
import React, { Component } from 'react';
import axios from 'axios';
import Autosuggest from 'react-autosuggest';
import './Home.css';
import history from './History';
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
const getSuggestionValue = suggestion => suggestion;
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion}
</div>
);
class Home extends Component {
constructor(props) {
super(props);
// Autosuggest is a controlled component.
// This means that you need to provide an input value
// and an onChange handler that updates this value (see below).
// Suggestions also need to be provided to the Autosuggest,
// and they are initially empty because the Autosuggest is closed.
this.state = {
value: '',
suggestions: [],
timeout: 0
};
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
console.log('=====++++ ' + newValue);
};
onSuggestionSelected = (event, { suggestion, suggestionValue, suggestionIndex, sectionIndex, method }) => {
console.log("Get the user +++++++++ " + suggestionValue);
if(suggestionValue && suggestionValue.length >= 1){
axios.get('https://api.github.com/users/'+ suggestionValue)
.then((response) => {
console.log("user selected : "+ response.data.avatar_url);
history.push({pathname: '/user', state: { detail: response.data }});
history.go('/user');
}).catch(function(error) {
console.log('Error on Authentication' + error);
});
}
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
if(this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.getSuggestions();
}, 500);
};
getSuggestions = () =>{
if(this.state.value && this.state.value.length >= 1){
axios.get('https://api.github.com/search/users',{
params: {
q: this.state.value,
in:'login',
type:'Users'
}
}).then((response) => {
console.log("users login : "+ response.data.items);
const userNames = response.data.items.map(item => item.login);
console.log("===== " + userNames);
this.setState({
suggestions: userNames
})
}).catch(function(error) {
console.log('Error on Authentication' + error);
});
}
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render(){
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: 'Type a userName',
value,
onChange: this.onChange
};
return (
<div>
<div>
Home page {this.props.location.state.detail.login}
</div>
<div>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
onSuggestionSelected={this.onSuggestionSelected}
/>
</div>
</div>
);
}
}
export default Home;
PrivateRouter.js
import React from 'react';
import {
Redirect,
Route,
} from "react-router-dom";
const PrivateRoute = ({ component: Component, ...rest}) => (
<Route
{...rest}
render={props =>
props.authenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/",
state: { from: props.location }
}}
/>
)
}
/>
);
export default PrivateRoute;
Can you help me how i can call onLoginAuth from axios?
I don't think that the problem comes from the axios call. Your axios call the dispatch which update the state authenticated. So everytime the state authenticated is updated, the render method of App component is called.
The condition if(this.props.authenticated) is verified, and Redirect to /home is called. But somehow, your router routes again to /. The App component is rerender. The condition if(this.props.authenticated) is true again, and the routes routes again to App. It creates an infinite loop, that's why you see the message Maximum update depth exceeded.
In the index.js, for testing purpose, replace all PrivateRoute by Route to see if the route works correctly. If it works, the problem may come from your PrivateRoute component.
I'm trying to make a simple authentication system in my Single Page Application.
I want to disable all routes execpt /login for guest.
The way to know if the user is authenticated or a guest it's to know if there are an access_token in the localStorage.
When I'm starting the app the Main component is started. The component define the routes and know if the user is authenticated by check the localStorage.
The default route (/) is for render the Home component but, like this example of react router the Home component is protected by a PrivateRoute object.
The PrivateRoute object check if the user is authenticated. If yes, Home component is rendered, else the user is redirected to the logincomponent at /login.
The Login component redirect the user to / in case of success and execute a callback to give the access_token.
The Main component define the callback, It's about to save the access_token in the localStorage and change the state for declare the user as authenticated. And now, the user can access the Home component.
My problem is, the PrivateRoute system always the check the user as a guest so it's always a redirection to /login. But when the are an access_token in localStorage the Login component redirect to the Home protected by the PrivateRoute and this is an infite loop despite the handleLogin callback.
Can you find a solution ?
Main.jsx
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter, Link, Redirect, Route} from "react-router-dom";
import {Login} from "./Login"
import {Home} from "./Home";
class Main extends Component {
constructor(props) {
super(props);
this.handleLogout = this.handleLogout.bind(this);
this.handleLogin = this.handleLogin.bind(this);
this.state = {authed: localStorage.getItem('access_token') !== null};
}
componentDidCatch(error, info) {
}
handleLogout(event) {
event.preventDefault();
localStorage.removeItem('access_token');
this.setState({authed: false});
}
handleLogin(token) {
localStorage.setItem('access_token', token);
this.setState({authed: token !== null});
}
render() {
const PrivateRoute = ({component: Component, ...rest}) => (
<Route {...rest} render={props =>
this.state.authed()
? (<Component {...props} />)
: (<Redirect to="/login"/>)
}
/>
);
const LoginLogout = () => {
return this.state.authed
? (<button onClick={this.handleLogout}>Logout</button>)
: (<Link to="/login">Login</Link>);
};
return (
<BrowserRouter>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<LoginLogout/>
</li>
</ul>
<Route path="/login" component={() => <Login handleLogin={this.handleLogin}/>}/>
<PrivateRoute exact path="/" component={Home}/>
</div>
</BrowserRouter>
);
}
}
if (document.getElementById('main')) {
ReactDOM.render(<Main/>, document.getElementById('main'));
}
Login.jsx
import React, {Component} from 'react';
import {Redirect} from "react-router-dom";
export class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
redirect: localStorage.getItem('access_token') !== null,
token: null,
loading: false,
error: null
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentWillUnmount() {
this.props.handleLogin(this.state.token);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
handleSubmit(event) {
event.preventDefault();
this.setState({
error: null,
loading: true
});
axios.post('/api/login', {
'client_id': '3',
'email': this.state.email,
'password': this.state.password,
'confirm_password': this.state.password
}).then((response) => {
let token = response.data.access_token;
this.setState({
redirect: true,
token: token,
loading: false
});
}, (error) => {
console.error('error', error.response.data);
this.setState({
error: error.response.data,
loading: false
});
});
}
render() {
if (this.state.redirect)
return (<Redirect to={"/"}/>);
return (
<form onSubmit={this.handleSubmit}>
<label htmlFor="email">Email :</label>
<input type="text" name="email" id="email" value={this.state.email} onChange={this.handleInputChange}
disabled={this.state.loading}/>
<label htmlFor="password">Password :</label>
<input type="password" name="password" id="password" value={this.state.password}
onChange={this.handleInputChange} disabled={this.state.loading}/>
<button type="submit"
disabled={this.state.loading}>{this.state.loading ? "..." : "Se connecter"}</button>
{this.state.error && (
<div>
<p>Erreur : {JSON.stringify(this.state.error)}</p>
</div>
)}
</form>
);
}
}
In your handleSubmit function you need to call the handleLogin from props so the state is correctly updated in your container component.
handleSubmit(event) {
...
.then((response) => {
let token = response.data.access_token;
this.setState({
redirect: true,
token: token,
loading: false
});
// here, call the handle from props
this.props.handleLogin(token);
}
...
That way, your this.state.authed will have the proper value
I'm building an admin app for a project with react, redux, react-router and react-router-redux. React-router is v4.0.0, react-router-redux is v5.0.0-alpha.3 (installed with npm install react-router-redux#next). What I'm trying is:
Load app,
Perform an async call to backend to see if the user is logged in (token stored in a cookie),
If user is not logged in, redirect to /login and render Login component.
For async actions I'm using redux-thunk.
Root.js
import React, { Component, PropTypes } from 'react';
import { Provider, connect } from 'react-redux';
import { Route, Switch } from 'react-router-dom';
import { ConnectedRouter, push } from 'react-router-redux';
import Login from './Login';
const App = () => <h1>Dashboard</h1>;
const NotFound = () => <h1>Not found :(</h1>;
class Root extends Component {
// use componentDidMount as recommended here:
// https://facebook.github.io/react/docs/react-component.html#componentdidmount
componentDidMount() {
const { dispatch, user } = this.props;
if (!user) {
dispatch(push('/login'));
}
}
render() {
const { store, history } = this.props;
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<Switch>
<Route exact path='/' component={App} />
<Route exact path='/login' component={Login} />
<Route component={NotFound} />
</Switch>
</div>
</ConnectedRouter>
</Provider>
);
}
}
Root.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
user: PropTypes.shape({
email: PropTypes.string.isRequired
})
};
const mapStateToProps = state => ({
ready: state.ready,
user: state.user
});
export default connect(mapStateToProps)(Root);
Login.js
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import {
loginFormChange,
loginFormSubmit
} from '../actions';
class Login extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
const { target } = event,
{ value, name } = target,
{ dispatch } = this.props;
dispatch(loginFormChange({
[name]: value
}));
}
handleSubmit(event) {
event.preventDefault();
const { dispatch, login } = this.props,
{ email, password } = login;
dispatch(loginFormSubmit({
email,
password
}));
}
render() {
const { login } = this.props,
{ email, password } = login;
return (
<form onSubmit={this.handleSubmit}>
<input type="email" name="email" value={email} onChange={this.handleChange} />
<input type="password" name="password" value={password} onChange={this.handleChange} />
<button type="submit">Sign in</button>
</form>
);
}
}
Login.propTypes = {
dispatch: PropTypes.func.isRequired,
login: PropTypes.shape({
email: PropTypes.string.isRequired,
password: PropTypes.string.isRequired
}).isRequired
};
const mapStateToProps = state => ({
login: state.login
});
export default connect(mapStateToProps)(Login);
actions.js
export const LOGIN_FORM_CHANGE = 'Login form change';
export const LOGIN_FORM_SUBMIT = 'Login form submit';
export const AUTHENTICATE_USER = 'Authenticate user';
export const loginFormChange = data => {
const { email, password } = data;
return {
type: LOGIN_FORM_CHANGE,
email,
password
};
};
export const loginFormSubmit = data => dispatch => {
const { email, password } = data;
return fetch('/api/auth/token', {
headers: {
'Authorization': 'Basic ' + btoa([ email, password ].join(':'))
},
credentials: 'same-origin'
})
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
})
.then(user => {
// this line will throw setState warning:
// Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
dispatch(authenticateUser(user));
});
};
export const authenticateUser = data => {
const { email } = data;
return {
type: AUTHENTICATE_USER,
email
};
};
I want to point out that I'm using the recommended approach to async actions, described in redux documentation. I won't post my reducers for brevity. Finally:
index.js
import React from 'react';
import { render } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import createHistory from 'history/createBrowserHistory';
import { routerMiddleware } from 'react-router-redux';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
import reducers from './reducers';
import Root from './containers/Root';
const history = createHistory(),
middleware = [
routerMiddleware(history),
thunk
];
if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger());
}
const store = createStore(
reducers,
applyMiddleware(...middleware)
);
render(
<Root store={store} history={history} />,
document.getElementsById('root')
);
So the warning gets thrown in the loginFormSubmit async action, when it tries to dispatch a sync authenticateUser action. Moreover it happens only after a redirect. I've tried different redirect approaches:
push from react-router-redux
Redirect component from react-router
I've also tried putting the redirect call in different places (componentWillMount, componentDidMount, componentWillReceiveProps, conditional rendering inside of the component, using conditional PrivateRoute components as described in the react-router documentation, etc.), but nothing seems to work.
If there is no redirect in the first place (e.g. a user opens /login page instead of a protected one), than there is no warning.
Any help on the issue is very much appreciated.
I am having the same issue and basically it's a bug with the ConnectedRouter from react-router-redux v5.0.0-alpha.2 and alpha.3
It was actively being discussed for the past few days but now it's fixed in alpha 4 and the issue is closed:
https://github.com/ReactTraining/react-router/issues/4713