I have a reactjs+redux app in which app.js, the first component to be mounted is given below:
//all imports here
class App extends React.Component {
constructor(){
super();
this.state = {
loginState : false,
}
}
componentDidMount() {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
console.log(user);
if(user.displayName&&user.photoURL&&user.email)
this.props.dispatch(login(user.displayName, user.photoURL, user.email));
else
this.props.dispatch(login(user.email.split("#")[0], "", ""));
this.setState({
loginState: true
});
}
else{
this.props.dispatch(changeLoading());
}
});
}
logout = () => {
firebase
.auth()
.signOut()
.then(() => {
this.setState({
loginState : false,
});
this.props.dispatch(logout());
this.props.history.push('/login');
})
.catch((err) => {
console.log(err);
});
};
render() {
return (
<>
<Switch>
{this.props.userName?(<Route
exact
path="/"
component={() => <Homepage userName={this.props.userName} />}
/>):(<Route exact path="/" component={Loading} />)}
<Route exact path="/login" component={Login} />
<Redirect to="/" />
</Switch>
</>
);
}
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.userState.isLoggedIn,
userName: state.userState.userName,
email: state.userState.email,
photoURL: state.userState.photoURL
};
};
export default withRouter(connect(mapStateToProps)(App));
Below is Homepage.js:
class Homepage extends React.Component{
componentDidMount(){
console.log("component mounted");
this.props.dispatch(fetchPosts());
}
render(){
console.log("Homepage reached");
if(this.props.userName==='') return <Redirect to="/login" />
return(
<div className="container-fluid m-0" style={{paddingTop: '100px',paddingLeft: '0',paddingRight: '0'}}>
<div className="row m-0">
<div class="col-md-1"></div>
<Main userName={this.props.userName} />
<Leftside userName={this.props.userName} />
<div class="col-md-1"></div>
</div>
</div>
);
}
}
const mapStateToProps=(state)=>({})
export default connect(mapStateToProps)(Homepage);
And below is the reducer:
export const userState=(state={isLoading: true,isLoggedIn: false,userName: '', photoURL: ''},action)=>{
switch(action.type){
case 'LOGIN': {console.log("reached");return {...state,isLoading: false,isLoggedIn: true, userName: action.payload.userName, photoURL: action.payload.photoURL, email: action.payload.email}}
case 'LOGOUT': return {...state,isLoading: false,isLoggedIn:false,userName: '',photoUrl: ''}
case 'CHANGE': return {...state,isLoading: false}
default: return {...state}
}
}
Basically what is happening is that initially when the app opens, this.props.userName is empty and hence Loading component is loaded. Once the firebase returns the user details, they are dispatched to redux reducer. When this happens, state.userState.userName becomes available and Homepage is mounted. As expected, its componentDidMount method runs and posts are fetched and dispatched to the redux store( posts are stored in redux store). But then suddenly Homepage unmounts and mounted again and consequently, componntDidMount runs again. So, in total, there are two fetchPost requests.
I do not understand this behaviour. I have read that componentDidMount runs only a single time.
Please help me to remove this bug.
Thank You!
Related
I'm using React and Redux. I'm new to React, but not to development. This is my first independent project after training. I want to really learn this and I don't understand what is happening right now.
I have a Delete page that I route to for List Items. I pass in the prop of list_item_id via Route. I'm getting that prop with no problem, and can access it properly through my method. This page is also using Modal, in case that's a factor.
Here's the Route section from App.js
return(
<div className="ui container">
<Router history={history}>
<div>
<Header />
<Switch>
<Route path="/" exact component={List} />
<Route path="/lists" exact component={List} />
<Route path="/list/:list_id" exact component={ListItemList} list_id={this.props.list_id} />
<Route path="/listitems/new/:list_id" exact component={ListItemCreate} />
<Route path="/listitems/edit/:list_item_id" exact component={ListItemEdit} />
<Route path="/listitems/delete/:list_item_id" exact component={ListItemDelete} />
<Route path="/listitems/:list_item_id" exact component={ListItemShow} />
</Switch>
</div>
</Router>
</div>
);
I'm getting the error onclick of the Delete button.
The prop that is undefined I'm getting from Redux via mapStatetoProps:
const mapStateToProps = (state, ownProps) => {
return {
listItem: state.listItems[ownProps.match.params.list_item_id]
};
};
When the page initially renders, it has the correct value. But when I click the Delete button, for some reason it's re-rendering, listItem is undefined, and it breaks.
Here's the component:
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import Modal from '../Modal';
import history from '../../history';
import { fetchListItem, deleteListItem } from '../../actions';
class ListItemDelete extends React.Component {
componentDidMount(){
this.props.fetchListItem(this.props.match.params.list_item_id);
}
renderActions(){
const list_item_id =this.props.match.params.list_item_id;
console.log('listItem');
console.log(this.props.listItem);
const list_id = this.props.listItem.list_id; // <--- breaking here
//console.log('list_id is ' + list_id);
return(
<React.Fragment>
<button onClick={() => this.props.deleteListItem(list_id, list_item_id)} className="ui button negative">Delete</button>
<Link to="/" className="ui button ">Cancel</Link>
</React.Fragment>
);
};
renderContent(){
if(!this.props.listItem){
return 'Are you sure you want to delete this list item?';
}
return `Are you sure you want to delete list item: ${this.props.listItem.list_item}?`;
}
render(){
return (
<Modal
title="Delete List Item"
content={this.renderContent()}
actions={this.renderActions()}
onDismiss={() => history.push("/")}
/>
);
}
}
const mapStateToProps = (state, ownProps) => {
return {
listItem: state.listItems[ownProps.match.params.list_item_id]
};
};
export default connect(mapStateToProps, { fetchListItem, deleteListItem })(ListItemDelete);
In case it's relevant, here's the action creator:
export const deleteListItem = (list_id, list_item_id) => async dispatch => {
let data = new FormData();
data.append('request', 'delete');
data.append('list_item_id', list_item_id);
await listItems.post(`/listItemsMaintenance`, data);
dispatch({ type: DELETE_LIST_ITEM, payload: list_item_id });
history.push(`/list/${list_id}`);
};
And the reducer (It's also not deleting properly, which is why I'm looking into it):
import _ from 'lodash';
import {
FETCH_LIST_ITEM,
FETCH_LIST_ITEMS,
CREATE_LIST_ITEM,
EDIT_LIST_ITEM,
DELETE_LIST_ITEM,
MARK_COMPLETED_LIST_ITEM
} from '../actions/types';
export default (state = {}, action) => {
const emptyObj = {};
switch (action.type) {
case FETCH_LIST_ITEMS:
return { ...state, ..._.mapKeys(action.payload, 'list_item_id') };
case FETCH_LIST_ITEM:
return { ...state, [action.payload.list_item_id]: action.payload };
case CREATE_LIST_ITEM:
return { ...state, [action.payload.list_item_id]: action.payload };
case EDIT_LIST_ITEM:
return { ...state, [action.payload.list_item_id]: action.payload };
case MARK_COMPLETED_LIST_ITEM:
return { ...state, [action.payload.list_item_id]: action.payload };
case DELETE_LIST_ITEM:
console.log('Now Reducing DELETE');
console.log(action.payload);
return _.omit(state, action.payload);
default:
return state;
}
};
And WHY is it calling the renderActions method so many times? Twice when it correctly gets the value of listItem and twice when it doesn't?? Here's an image of the error and console.
I am getting "user" from "decoded token" and setting that user in state, but value is not getting store in state although "user" has a value.
Here is my code.
class Complainer extends Component {
state = {};
componentDidMount() {
const user = auth.getCurrentUser();
console.log(user);
this.setState({ user });
if (!user) window.location = '/';
}
but user is not getting stored in state. please help.
Try this code:
class Complainer extends Component
{
state = {
user: '',
};
componentDidMount(){
const user = auth.getCurrentUser();
console.log(user);
this.setState({
user: user
});
}
render(){
return (
<React.Fragment>
<Navbar />
<main className="container">
<Switch>
<Route path="/complainer/view-all" component={AllComplaints} />
<Route path="/complainer/not-found" component={notfound} />
<Showcase user={this.state.user} />
</Switch>
</main>
</React.Fragment>
);
}
I am trying to make a login page which will redirect to the home page after successful authentication. Here is the code.
routes.js
const createRoutes = (store, history) => {
return (
<Router history={history}>
<div>
<AppLayout/>
<Route exact path="/" component={Home}/>
<Route path="/login" component={LoginContainer}/>
<Route path="/register" component={RegisterContainer}/>
</div>
</Router>
);
};
export default createRoutes;
actions.js
export const login = (data, successPath) => {
return (dispatch) => {
dispatch(beginLogin());
return makeUserRequest("post", "/login", data)
.then((resp) => {
if (resp.data.success) {
dispatch(loginSuccess(data));
browserHistory.push(successPath);
} else {
dispatch(loginError(resp.data));
}
})
.catch(console.error);
};
};
Login.js
class Login extends React.Component {
login = (event) => {
event.preventDefault();
const email = event.target.querySelector("input[name='email']").value;
const password = event.target.querySelector("input[name='password']").value;
this.props.login({
email,
password
}, this.props.nextPathName);
}
render() {
return (
<div className="Login">
<h2>Login</h2>
<form onSubmit={this.login}>
<input type="email" name="email" placeholder="Email"/>
<input type="password" name="password" placeholder="Password"/>
<input type="submit" value="Login"/>
<h1>{this.props.user.email}</h1>
</form>
</div>
);
}
}
Login.propTypes = {
login: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
nextPathName: PropTypes.string.isRequired
};
export default Login;
LoginContainer.js
const mapStateToProps = (state, ownProps) => {
let nextPathName = "/";
try {
nextPathName = ownProps.location.state.nextPathName;
} catch(err) {
// ignore
}
return {
user: state.user,
nextPathName
};
};
export default withRouter(connect(mapStateToProps, userActions)(Login));
reducer.js
// reducer for user actions
const user = (state = { isWaiting: false, authenticated: false, email: "", message: "" }, action) => {
switch(action.type) {
case REGISTER_USER:
return { ...state, isWaiting: true };
case REGISTER_SUCCESS_USER:
return { ...state, isWaiting: false, message: action.data.message };
case REGISTER_ERROR_USER:
return { ...state, isWaiting: false, message: action.data.message };
case LOGIN_USER:
return { ...state, isWaiting: true };
case LOGIN_SUCCESS_USER:
return { ...state, isWaiting: false, authenticated: true, email: action.data.email };
case LOGIN_ERROR_USER:
return { ...state, isWaiting: false };
default:
return state;
}
};
export default user;
export default combineReducers({
user,
routing: routerReducer
});
The login functionality is working correctly and when I am clicking the login button, the email in the component is printing correctly. After that the browser is changing to the new location which is "/", but the page is not being rendered, i.e. the view is remaining same even after the browser location has changed. I have looked into similar question answers on stackoverflow, but have not found any solution to my problem. I have even used withRouter but to no solution. Any help will be appreciated.
I think you are supposed to use one of the higher level Router components like : <BrowserRouter> if your web/app is hosted on a dynamic server
or <HashRouter> if your webs/app is hosted on a static server
routes.js
import {BrowserRouter, Route} from 'react-router-dom';
const createRoutes = (store, history) => {
return (
<BrowserRouter>
<div>
<AppLayout/>
<Route exact path="/" component={Home}/>
<Route path="/login" component={LoginContainer}/>
<Route path="/register" component={RegisterContainer}/>
</div>
<BrowserRouter>
);
};
export default createRoutes;
<BrowserRouter> is just a wrapper with a pre-built history so you can do same with custom history instance . You can read more Here
Instead of using the imperative push method, you could use the nice declarative <Redirect /> component that comes with React Router:
{user.authenticated && <Redirect to={nextPath} />}
You can place this wherever it makes sense for you. Inside your Login component or at the top level (just making sure not to redirect over and over)
I am new to SPA development. I use React and React Router and made simple app. It has two pages: public and protected. User can see protected page only when he was signed in. I use firebase for managing users.
The problem is when I go to protected page, log in and can see its content, I then resresh the page and redirected to "default" state which is public page.
index.js
ReactDOM.render(<App />, document.getElementById('root'));
App.js
const ProtectedPage = () => {
return (
<div>
<h1> Protected page. </h1>
<Link to="/">Public resources</Link>
</div>
);
}
const PublicPage = () => {
return (
<div>
<h1> Public page. </h1>
<p> Login to see protected resources </p>
<Link to="/protected">Protected resources</Link>
</div>
);
}
const PrivateRoute = ({ component: Component, isAuthenticated, ...rest }) => (
<Route {...rest} render={props => (
isAuthenticated ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/',
state: { from: props.location }
}}/>
)
)}/>
)
class App extends React.Component {
constructor(props) {
super(props);
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.state = {
username: '',
uuid: ''
}
}
login() {
auth.signInWithEmailAndPassword(email, password).then((user) => {
console.log('Sign in ', user.displayName);
this.setState({
username: user.displayName,
uuid: user.uuid
});
});
}
logout() {
auth.signOut().then(() => {
console.log('Sign out ');
this.setState({
username: '',
uuid: ''
});
})
}
componentDidMount() {
this.releaseFirebaseAuthHandler = auth.onAuthStateChanged((user) => {
if(user) {
this.setState({
username: user.displayName,
uuid: user.uuid
});
}
});
}
componentWillUnmount() {
this.releaseFirebaseAuthHandler();
}
render() {
return (
<div>
<div className="navbar">
<nav className="navbar-nav">
{ this.state.username ?
<button onClick={this.logout}>Logout</button> :
<button onClick={this.login}>Login</button> }
</nav>
</div>
<div>
<div>
<Route exact path="/" component={PublicPage} />
<PrivateRoute path="/protected" component={ProtectedPage} isAuthenticated={this.state.uuid !== ''} />
</div>
</div>
</div>
);
}
}
export default App;
Is it possible to preserve localhost:3000/protected page after refreshing it without involving the server?
In your code you redirect to pathname: '/' if user is not authenticated:
const PrivateRoute = (...) => (
<Route {...rest} render={props => (
isAuthenticated
? <Component {...props}/>
: <Redirect to={{
pathname: '/',
state: { from: props.location }
}}/>
)}/>
)
If you would like to keep /protected in url one way to do it is to replace redirect with something like <div>Please login</div>
The state is not preserved across multiple sessions, for this purpose you can use the localStorage.
I've created a login page that takes a user from public to authenticated routes which works well. If there is an error with login (eg. email not found) I get the error in console Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the LoginForm component..
I think it might be related to how I use createContainer in the App.
I believe the problem is related to the way meteor passes Meteor.loggingIn(); equals true before it has heard back from the server. If there is an error the true quickly changes to false, which I think leads to the page reloading.
I would like to be able to use this.setState({ loginError: error.reason }); so I can tell the user what went wrong.
Any suggestions.
Path: App.jsx
const App = appProps => (
<Router>
<Grid className="main-page-container">
<Switch>
<Authenticated exact path="/" component={Home} {...appProps} />
<Public exact path="/login" component={Login} {...appProps} />
</Switch>
</Grid>
</Router>
);
App.propTypes = {
loggingIn: PropTypes.bool,
authenticated: PropTypes.bool
};
export default createContainer(() => {
const loggingIn = Meteor.loggingIn();
return {
loggingIn,
authenticated: !loggingIn && !!Meteor.userId()
};
}, App);
Path: Public.jsx
const Public = ({ loggingIn, authenticated, component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return !authenticated ?
(React.createElement(component, { ...props, loggingIn, authenticated })) :
(<Redirect to="/" />);
}}
/>
);
Public.propTypes = {
loggingIn: PropTypes.bool,
authenticated: PropTypes.bool,
component: PropTypes.func
};
export default Public;
Path: LoginForm.jsx
export default class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
errors: {},
password: '',
loginError: ''
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
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({
errors: {}
}, function() {
var data = {
email: this.state.email,
password: this.state.password
};
var email = this.state.email;
var password = this.state.password;
const errors = loginUserValidation(data);
if (errors) {
this.setState({errors: errors});
} else {
Meteor.loginWithPassword(email, password, (error) => {
if (error) {
this.setState({ loginError: error.reason });
}
});
}
});
}
render() {
return (
<div className="registration-form-container">
<Row>
<Col sm={8} smOffset={2} md={6} mdOffset={3}>
<div className="paper">
<Form onSubmit={this.handleSubmit}>
<section className="form-title">
<h3 className="text-center">Login</h3>
</section>
<hr />
<section className="form-content-login-or-registration">
{this.state.loginError &&
<div className="alert alert-danger">
<p>{this.state.loginError}</p>
</div>
}
<SingleInput
name={'email'}
inputType={'email'}
controlFunc={this.handleInputChange}
content={this.state.email}
placeholder={'Email'}
bsSize={null}
error={this.state.errors && this.state.errors.email}
/>
<SingleInput
name={'password'}
inputType={'password'}
controlFunc={this.handleInputChange}
content={this.state.password}
placeholder={'Password'}
bsSize={null}
error={this.state.errors && this.state.errors.password}
/>
</section>
<section className="form-buttons">
<Button type="submit" className="btn btn-primary" block>Login</Button>
</section>
</Form>
</div>
</Col>
</Row>
</div>
)
}
}
When Meteor.loginWithPassword changes your logginIn value to true, your <Public /> component unmounts your wrapped <LoginForm />
const Public = ({ loggingIn, authenticated, component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />; // <--- this right here
return !authenticated ?
(React.createElement(component, { ...props, loggingIn, authenticated })) :
(<Redirect to="/" />);
}}
/>
);
So loggingIn values changes causing <Public /> to re-render. Now that loggingIn is true, you render a div instead of the component that was, unmounting it and making setStateunavailable when the errors callback tries to invoke it.
EDIT: In response to your comment...
To prevent this you can handle the error display inside the <LoginForm />
Remove if (loggingIn) return <div />; from your <Route /> in your <Public /> component.
Inside your <LoginForm /> you can handle the error condition. Do this by making a component that displays your errors and include it in your <LoginForm /> component where you want it displayed. Make the <ErrorDisplay /> component return nothing if there are no errors.
Example <ErrorDisplay /> component
const ErrorDisplay = ({ errors }) => {
errors && <div className="error-container">{ errors }</div>
};
That is obviously barebones, but I hope it helps you understand!