call mapDispatchToProps inside axios success not working - javascript

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.

Related

React Redux Class Component TypeError: Cannot read properties of undefined (reading 'params')

I get the error TypeError: Cannot read properties of undefined (reading 'params')
so I try to get data detail users using id, and when I try to see, I get the error
app.js
import React, { Component } from 'react'
import Jumbotron from './component/Jumbotron'
import NavbarComponent from './component/NavbarComponent'
import {
BrowserRouter,
Route,
Routes,
} from "react-router-dom";
import HomeContainer from './container/HomeContainer';
import CreateUserContainer from './container/CreateUserContainer';
import EditUserContainer from './container/EditUserContainer';
import DetailUserContainer from './container/DetailUserContainer';
export default class App extends Component {
render() {
return (
<div>
<BrowserRouter>
<NavbarComponent />
<Jumbotron />
<Routes>
<Route path='/' element={<HomeContainer/>} exact />
<Route path='/create' element={<CreateUserContainer />} exact />
<Route path='/edit/:id' element={<EditUserContainer />} exact />
<Route path='/detail/:id' element={<DetailUserContainer />} exact />
</Routes>
</BrowserRouter>
</div>
)
}
}
container/DetailUserContainer.js
import React, { Component } from 'react'
import { Container } from 'react-bootstrap'
import { connect } from 'react-redux';
import { getDetailUser } from '../actions/UserAction';
class DetailUserContainer extends Component {
componentDidMount(){
this.props.dispatch(getDetailUser(this.props.match.params.id));
}
render() {
return (
<div>
<Container className='pt-3'>
<div>
<h1>Detail</h1>
</div>
</Container>
</div>
)
}
}
export default connect()(DetailUserContainer)
reducers/users.js
import { GET_LIST_USERS, GET_DETAIL_USERS } from "../actions/UserAction";
let initialState = {
getUsers: false,
errorGetUser: false,
// DETAIL USER
getDetailUsers: false,
errorGetDetailUser: false
}
const users = (state = initialState, action) => {
switch (action.type) {
case GET_LIST_USERS:
return {
...state,
getUsers: action.payload.data,
errorGetUser: action.payload.errorMessage
};
case GET_DETAIL_USERS:
return {
...state,
getDetailUsers: action.payload.data,
errorGetDetailUser: action.payload.errorMessage
};
default:
return state;
}
}
export default users;
actions/UserAction.js
import React from 'react'
import axios from 'axios'
export const GET_LIST_USERS = "GET_LIST_USERS"
export const GET_DETAIL_USERS = "GET_DETAIL_USERS"
// GET USER
export const getListUser= () => {
return (dispatch) => {
axios.get('https://my-json-server.typicode.com/baihaqiyazid/database/users')
.then(function (response) {
dispatch({
type: GET_LIST_USERS,
payload: {
data: response.data,
errorMessage: false
}
})
})
.catch(function (error) {
dispatch({
type: GET_LIST_USERS,
payload: {
data: false,
errorMessage: error.message
}
})
})
}
}
// DETAIL USER
export const getDetailUser= (id) => {
return (dispatch) => {
axios.get('https://my-json-server.typicode.com/baihaqiyazid/database/users/' + id)
.then(function (response) {
dispatch({
type: GET_DETAIL_USERS,
payload: {
data: response.data,
errorMessage: false
}
})
})
.catch(function (error) {
dispatch({
type: GET_DETAIL_USERS,
payload: {
data: false,
errorMessage: error.message
}
})
})
}
}
reducers/index.js
import { combineReducers } from 'redux'
import users from './users'
export default combineReducers({
users
})
How to solve it?
In your component did mount use this..
componentDidMount(){
// #id params
this.props.getDetailUser(1);
}
Also connect your state using mapstate..
function mapState(state) {
return {
match: state.users // which is the name you have assigned in the combine Reducer
};
}
export default connect(mapState, {getDetailUser})(DashBoard);
refer..
Class component with Redux
https://react-redux.js.org/tutorials/connect
Before version 6, react-router-dom's Route components passed a few routing properties to any component it rendered, for example you had a route like this:
<Route path='/' component={HomeContainer} exact />
You had access to route props inside HomeContainer, like this:
this.props.match.params.id
In the above line, match is one of route props, passed from Route down to HomeContainer.
After Version 6, (which you are using), no such thing happens. The route props are no longer passed down to children, so you don't have access to this.props.match, or other route props (location, etc...).
Instead, you need to use hooks in order to have access to match, which you can't use inside Class Components. Two solutions are available:
If you want to stick to classes, Change react-router-dom version from v6 to v5, (then you have to use components like Route according to v5 API).
Get rid of Classes, instead use Functional Components, have access to hooks and keep using react-router-dom v6.

What is the correct way of redirecting after successful post request in React?

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')

Component rendering too early

I am trying to create a PrivateRoute(HOC) to test if a user has been authenticated(check is 'auth' exist in redux store) before sending them to the actual route. The issue is the privateroute finishes before my auth shows up in redux store.
The console.log runs twice, the first time, auth doesnt appear in the store, but it does the second time, but by that time, its already routed the user to the login screen.... How can I give enough time for the fetch to finish? I know how to do this condition when I simply want to display something conditionally(like login/logout buttons) but this same approach does not work when trying to conditionally route someone.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Route } from 'react-router-dom'
class PrivateRoute extends Component {
render() {
const { component: Component, ...rest } = this.props
console.log(this.props)
return (
<Route {...rest} render={(props) => (props.auth ? <Component {...props} /> : props.history.push('/login'))} />
)
}
}
function mapStateToProps({ auth }) {
return { auth }
}
export default connect(mapStateToProps)(PrivateRoute)
I didn't use redux here, but I think you would get the main point. Hope this will help and feel free to ask any questions!
import React, { Component } from "react";
import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";
import Dashboard from "path/to/pages/Dashboard";
class App extends Component {
state = {
isLoggedIn: null,
};
componentDidMount () {
// to survive F5
// when page is refreshed all your in-memory stuff
// is gone
this.setState({ isLoggedIn: !!localStorage.getItem("sessionID") });
}
render () {
return (
<BrowserRouter>
<Switch>
<PrivateRoute
path="/dashboard"
component={Dashboard}
isLoggedIn={this.state.isLoggedIn}
/>
<Route path="/login" component={Login} />
{/* if no url was matched -> goto login page */}
<Redirect to="/login" />
</Switch>
</BrowserRouter>
);
}
}
class PrivateRoute extends Component {
render () {
const { component: Component, isLoggedIn, ...rest } = this.props;
return (
<Route
{...rest}
render={props =>
isLoggedIn ? <Component {...props} /> : <Redirect to="/login" />
}
/>
);
}
}
class Login extends Component {
state = {
login: "",
password: "",
sessionID: null,
};
componentDidMount () {
localStorage.removeItem("sessionID");
}
handleFormSubmit = () => {
fetch({
url: "/my-app/auth",
method: "post",
body: JSON.strigify(this.state),
})
.then(response => response.json())
.then(data => {
localStorage.setItem("sessionID", data.ID);
this.setState({ sessionID: data.ID });
})
.catch(e => {
// error handling stuff
});
};
render () {
const { sessionID } = this.state;
if (sessionID) {
return <Redirect to="/" />;
}
return <div>{/* login form with it's logic */}</div>;
}
}
When your action creator return the token, you need to store it in localStorage. and then you can createstore like below,
const store = createStore(
reducers,
{ auth: { authenticated : localStorage.getItem('token') }},
applyMiddleware(reduxThunk)
)
if user already logged in then token will be there. and initial state will set the token in store so you no need to call any action creator.
Now you need to secure your components by checking if user is logged in or not. Here's the HOC for do that,
import React, { Component } from 'react';
import { connect } from 'react-redux';
export default ChildComponent => {
class ComposedComponent extends Component {
componentDidMount() {
this.shouldNavigateAway();
}
componentDidUpdate() {
this.shouldNavigateAway();
}
shouldNavigateAway() {
if (!this.props.auth) {
this.props.history.push('/');
}
}
render() {
return <ChildComponent {...this.props} />;
}
}
function mapStateToProps(state) {
return { auth: state.auth.authenticated };
}
return connect(mapStateToProps)(ComposedComponent);
};

React login authentication in child components

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?

React-router-redux setState warning after redirect

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

Categories