I have a login component that stores the user information in the global state after a successful login. The login component is pretty straight forward. It contains a form with a handleSubmit event that calls an endpoint. Based on the result of that endpoint an action is taken. The login component looks like this.
import React, { Component } from 'react';
import { StateContext } from '../state';
import { login } from '../repositories/authenticationRepository';
class Login extends Component {
static contextType = StateContext;
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
message: '',
};
}
handleChange = (event) => {
const { name, value } = event.target;
this.setState({ [name]: value });
}
handleSubmit = async (event) => {
event.preventDefault();
const [{}, dispatch] = this.context;
const { history } = this.props;
const { email, password } = this.state;
const isLoggedInResponse = await login({ email, password });
if (isLoggedInResponse.data.type === 'error') {
this.setState({ message: isLoggedInResponse.data.message });
return;
}
dispatch({ type: 'storeUserInformation', userInformation: isLoggedInResponse.data.message });
history.push('/');
}
render() {
const { email, password, message } = this.state;
return (
<div className="login-wrapper">
<form onSubmit={this.handleSubmit}>
<label htmlFor="email">
Email:
<input autoComplete="off" name="email" type="text" value={email} onChange={this.handleChange} />
</label>
<label htmlFor="password">
Password:
<input autoComplete="off" id="password" name="password" type="password" value={password} onChange={this.handleChange} />
</label>
{message.length > 0 && <span className="text-danger error">{message}</span> }
<input className="btn btn-secondary" type="submit" value="Submit" />
</form>
</div>
);
}
}
export default Login;
When testing it myself I can see the user information being set in the ReactJS devtools. Of course I want to test this automatically using a unit test, so I wrote the following.
jest.mock('../../repositories/authenticationRepository');
import React from 'react';
import { mount } from 'enzyme';
import Login from '../../pages/Login';
import { StateProvider } from '../../state';
import { login } from '../../repositories/authenticationRepository';
import { act } from 'react-dom/test-utils';
import history from '../../sitehistory';
import { BrowserRouter as Router } from 'react-router-dom';
import { reducer } from '../../reducer';
it('Saves the user information in the store on a succesfull login', async () => {
login.mockReturnValue(({ data: { type: 'success', message: 'Message should be stored' }}));
let initialStateMock = {}
const wrapper = mount(
<StateProvider initialState={initialStateMock} reducer={reducer}>
<Router>
<Login history={history} />
</Router>
</StateProvider>
);
let emailEvent = { target: { name: 'email', value: 'test#example.com'} }
let passwordEvent = { target: { name: 'password', value: 'password'} }
wrapper.find('input').first().simulate('change', emailEvent);
wrapper.find('input').at(1).simulate('change', passwordEvent);
const submitEvent = { preventDefault: jest.fn() }
await act(async () => {
wrapper.find('form').first().simulate('submit', submitEvent);
});
act(() => {
wrapper.update();
});
console.log(initialStateMock); // expected { userInformation: 'Message should be stored' } but got {}
});
I expect the initialStatemock to have the value of { userInformation: 'Message should be stored' }. However it still has the initial value of {}. I tried wrapper.update() to force a refresh but to no avail. What am I overlooking?
Related
I'm trying to build simple login form (data for authorization from API). So I have Slice for auth here is code :
auth.js
import { createSlice } from '#reduxjs/toolkit'
export const authSlice = createSlice({
name: 'auth',
initialState: {
isLoggedIn: false,
token: null,
role: null
},
reducers: {
logIn: (state) => {
state.isLoggedIn = true
},
role:(state, action) =>{
state.role = action.payload
},
token:(state, action) =>{
state.token = action.payload
},
logOut: (state) => {
state.isLoggedIn = false
},
},
})
export default authSlice.reducer;
export const { logIn, logOut, role, token } = authSlice.actions
authService.js :
import axios from 'axios';
import { api } from '../api/index'
export function authenticateUser(username, password) {
axios
.post(api + 'login', {username, password})
.then(res => {
console.log(res.headers.authorization)
})
}
LoginForm.js
import React, { Component } from 'react';
import { Form, Col, Button } from 'react-bootstrap';
import { IoMdLogIn } from "react-icons/all";
import { authenticateUser } from '../services/authService'
export default class LoginForm extends Component{
constructor(props) {
super(props);
this.state = this.initialState;
this.credentialsChange = this.credentialsChange.bind(this);
this.userLogin= this.userLogin.bind(this);
}
initialState = {
username: '', password: ''
}
userLogin = (e) => {
e.preventDefault();
authenticateUser(this.state.username, this.state.password);
this.setState( () => this.initialState)
}
credentialsChange = e => {
this.setState({
[e.target.name]:e.target.value
});
}
render(){
const {username, password} = this.state;
return(
<Form onSubmit={this.userLogin} id="loginFormId">
<Form.Row>
<Form.Group as={Col} controlId="formGridCountry">
<Form.Label>Username</Form.Label>
<Form.Control required autoComplete="off"
type="text" name="username"
value={username}
onChange={this.credentialsChange}
className={"bg-light"}
placeholder="Username" />
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col} controlId="formGridZipCode">
<Form.Label>Password</Form.Label>
<Form.Control required autoComplete="off"
type="password" name="password"
value={password}
onChange={this.credentialsChange}
className={"bg-light"}
placeholder="Password" />
</Form.Group>
</Form.Row>
<Button type="submit" variant="success">
<IoMdLogIn />
</Button>
</Form>
);
}
}
What I'm trying to reach is : I want to update state isLoggedIn : true after calling function authenticateUser.
I've tried to use const dispatch = useDispatch() and then calling dispatch(logIn()) but it's throwing error.
Where should I call dispatcher to update state?
You need to call the dispatcher in your AuthService.js in the api response.
Check the response, if it is ok, store it. If your redux is well implemented, it will work.
axios
.post(api + 'login', {username, password})
.then(res => {
console.log(res.headers.authorization)
//Call it here, or create a function and call it here.
})
}
If it doesn't work, please share the error with us
i've been trying since days to redirect my user after login to the home creating a callback function in my App.js and sending it as props to the login class component throught a loginregisterpage class component, but this doesn't work, can someone have a look on it and tell me what i;m missing out?
Thank you my code look like this
App.js
import React from 'react'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import { HomePage } from './Pages/HomePage/HomePage'
import { LoginRegisterPage } from './Pages/LoginRegisterPage/LoginRegisterPage'
import 'bootstrap/dist/css/bootstrap.min.css'
export class App extends React.Component {
constructor(props) {
super(props);
this.state = {
authenticated: false,
}
this.handleSuccess = this.handleSuccess.bind(this);
}
handleSuccess = (data) => {
this.props.history.push("/")
}
render() {
return (
<Router>
<Switch>
<Route exact path="/">
<HomePage />
</Route>
<Route exact path="/login-register">
<LoginRegisterPage onLoginSuccess={this.handleSuccess} />
</Switch>
</Router>
)
}
}
LoginRegisterPage class component
class LoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
accessToken: '',
authenticated: ''
};
this.handleChangeUsername = this.handleChangeUsername.bind(this);
this.handleChangePassword = this.handleChangePassword.bind(this);
}
handleChangeUsername(event) {
this.setState({
username: event.target.value
})
}
handleChangePassword(event) {
this.setState({
password: event.target.value
})
}
handleClick(event) {
var apiBaseUrl = "https://myapi.com/auth/"
const payload = {
method: "POST",
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify({
'username': this.state.username,
'password': this.state.password
})
};
const { username, password } = this.state;
if (username && password) {
fetch(apiBaseUrl + 'login', payload)
.then((response) => {
if (response.status === 200) {
alert("Logged In! You'll be redirected on Home")
return response.json()
} else {
return alert("wrong pass")
}
}).then((data) => {
this.setState({
accessToken: data.accestToken,
authenticated: data.authenticated
});
localStorage.setItem('accessToken', data.accessToken);
if (data.authenticated === true) {
console.log(this.props)
this.props.onLoginSuccess(data)
}
})
.catch((err) => console.log(err));
} else {
alert("Cannot be Empty")
}
}
render() {
return (
<div>
<div className="form">
<div>
<div className="form-input">
<div >
<div className="userData">
<span>
<img
src={UserIcon}
/>
</span>
<input
autocomplete="off"
type="text"
name="username"
placeholder="Username"
value={this.state.username}
onChange={this.handleChangeUsername}
/>
</div>
<div className="userData">
<span>
<img
src={PasswordIcon}
/>
</span>
<input
autocomplete="off"
type="password"
name="password"
placeholder="Password"
value={this.state.password}
onChange={this.handleChangePassword}
/>
<p style={(this.state.username && this.state.password) ? { display: 'none' } : { display: 'block' }}> Must fill all the form!</p>
</div>
</div>
</div>
</div>
</div>
<div className="form-footer">
<img
src={Btn}
onClick={(event) => this.handleClick(event)}
/>
</div>
</div>
);
}
}
LoginPage class component
class LoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
accessToken: '',
authenticated: ''
};
this.handleChangeUsername = this.handleChangeUsername.bind(this);
this.handleChangePassword = this.handleChangePassword.bind(this);
}
handleChangeUsername(event) {
this.setState({
username: event.target.value
})
}
handleChangePassword(event) {
this.setState({
password: event.target.value
})
}
handleClick(event) {
var apiBaseUrl = "https://movies-app-siit.herokuapp.com/auth/"
const payload = {
method: "POST",
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify({
'username': this.state.username,
'password': this.state.password
})
};
const { username, password } = this.state;
if (username && password) {
fetch(apiBaseUrl + 'login', payload)
.then((response) => {
if (response.status === 200) {
alert("Logged In! You'll be redirected on Home")
return response.json()
} else {
return alert("wrong pass")
}
}).then((data) => {
this.setState({
accessToken: data.accestToken,
authenticated: data.authenticated
});
localStorage.setItem('accessToken', data.accessToken);
if (data.authenticated === true) {
console.log(this.props)
this.props.onLoginSuccess(data)
}
})
.catch((err) => console.log(err));
} else {
alert("Cannot be Empty")
}
}
render() {
return (
<div>
<div className="form">
<div>
<div className="form-input">
<div >
<div className="userData">
<span>
<img
src={UserIcon}
/>
</span>
<input
autocomplete="off"
type="text"
name="username"
placeholder="Username"
value={this.state.username}
onChange={this.handleChangeUsername}
/>
</div>
<div className="userData">
<span>
<img
src={PasswordIcon}
/>
</span>
<input
autocomplete="off"
type="password"
name="password"
placeholder="Password"
value={this.state.password}
onChange={this.handleChangePassword}
/>
<p style={(this.state.username && this.state.password) ? { display: 'none' } : { display: 'block' }}> Must fill all the form!</p>
</div>
</div>
</div>
</div>
</div>
<div className="form-footer">
<img
src={Btn}
onClick={(event) => this.handleClick(event)}
/>
</div>
</div>
);
}
}
If you're using React Router you can use the Redirect component:
import { Redirect } from 'react-router-dom';
export default function PrivateRoute () {
if (notLoggedIn()) {
return <Redirect to="/login"/>;
}
// return your component
}
But if you're not inside a render function (i.e. you're in a submit callback) or you want to rewrite browser history, use the useHistory hook (note: hooks work only in function components, not class components)
import { useHistory } from 'react-router-dom';
const history = useHistory();
// After your login action you can redirect with this command:
history.push('/otherRoute');
Issue
App is defined outside the Router component so it has no history prop function to call to do any navigation.
Solution
Have the LoginRegisterPage component navigate upon successful authentication. It will need to access the history object of the nearest Router context. Normally this is achieved by consuming passed route props from the Route component.
You can:
#1
Move LoginRegisterPage to be rendered by the component prop of the Route so it receives the route props and thus the history object as a prop.
<Route exact path="/login-register" component={LoginRegisterPage} />
LoginRegisterPage
class LoginPage extends React.Component {
constructor(props) {
...
}
...
handleClick(event) {
var apiBaseUrl = "https://myapi.com/auth/"
const payload = {...};
const { username, password } = this.state;
const { history } = this.props; // <-- destructure history from props
if (username && password) {
fetch(apiBaseUrl + 'login', payload)
.then((response) => {
...
}).then((data) => {
this.setState({
accessToken: data.accestToken,
authenticated: data.authenticated
});
localStorage.setItem('accessToken', data.accessToken);
if (data.authenticated === true) {
console.log(this.props)
this.props.history.push("/"); // <-- navigate!
}
})
.catch((err) => console.log(err));
} else {
alert("Cannot be Empty")
}
}
render() {
...
}
}
#2
Decorate your LoginRegisterPage with the withRouter Higher Order Component so the route props are injected as props.
import { withRouter } from 'react-router-dom;
...
const LoginPageWithRouter = withRouter(LoginPage);
Note
If you prefer to do a redirect then replace any history.push calls with history.replace. push is a normal navigation and pushes on a new path on the history state whereas replace replaces the current history entry in the stack. After the auth redirect you probably don't want users to back navigate back to your login page/route.
Edit
If you need the handleSuccess callback to manage some auth state in App then I think it best to let App manage the authentication state and the LoginPage to still handle navigation. In this case, go with the second solution above so it receives both the handleSuccess callback and the history object.
if (data.authenticated === true) {
this.props.onLoginSuccess(data); // <-- callback to parent to set state
this.props.history.replace("/"); // <-- imperative navigation
}
Define your handleSucess function in LoginRegisterPage instead of passing it as a prop and this should work.
I am developing a login/register system using Redux,Hooks and Axios the action should fetch the backend and return a session to be updated in my reducer , after that I am trying to console.log(session) from my component the first time it is {} empty 'The initial state of session' is consoled the second time it is updated , I checked my redux state and everything works good and the state is updated from the first time so not a redux issue .The problem is in my component as I need it to wait for the Redux finishing and then console.log() , I tried to setTime() but it waits for the given time and after that It console the initial state {} again.
My code:
Component:
The problem is in the Redirect() function
import { LoginAction } from "../../Redux/Actions";
import { useForm } from "react-hook-form";
import { connect } from "react-redux";
const Login = (props) => {
let history = useHistory();
const alert = useAlert();
const { handleSubmit, register, errors } = useForm();
const onSubmit = (data) => {
const userData = {
username: data.username.toUpperCase(),
password: data.password,
};
props.login(userData);
setTimeout(1000, Redirect());
};
const Redirect = () => {
if (props.session.user) {
console.log(props.session);
sessionStorage.setItem("storedSession", props.session.user.username);
history.push("/dashboard");
} else {
alert.show(<div style={{ size: "10px" }}>{props.session.error}</div>);
}
};
return (
<div className="login">
<div className="login-form">
<h3 className="title">Sign In</h3>
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="text"
placeholder="Enter your username"
name="username"
id="username"
ref={register({ required: true })}
/>
{errors.username && errors.username.type === "required" && (
<p className="error-before-submit">This is required</p>
)}
<input
id="password"
placeholder="Enter your password"
name="password"
type="password"
ref={register({ required: true })}
/>
{errors.password && errors.password.type === "required" && (
<p className="error-before-submit">This is required</p>
)}
<input
className="btn"
type="submit"
ref={register({ required: true })}
/>
<a href="/register" className="register-link">
Create an account
</a>
</form>
</div>
</div>
);
};
const mapStateToProps = (state) => ({
session: state.Session,
});
const mapDispatchToProps = (dispatch) => ({
login: (user) => dispatch(LoginAction(user)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Login);
Reducer:
const initialState = {
Session: {},
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case Register:
return {
...state,
Session: action.payload,
};
case Login:
return {
...state,
Session: action.payload,
};
default:
return state;
}
};
export default rootReducer;
Action:
export function LoginAction(user) {
return (dispatch) => {
Axios.post(
"/api/login",
{
username: user.username,
password: user.password,
},
{
headers: { "Access-Control-Allow-Origin": "*" },
withCredentials: true,
crossdomain: true,
}
).then((res) => {
dispatch({
type: Login,
payload: res.data,
});
});
};
}
How can I make my component take the updated state not the initial state FROM FIRST CLICK ??
You can leverage useEffect hook instead of using timeout
const {session} = props;
useEffect(()=>{
Redirect()
},[session])
I am using React + Next.js + Firebase.Auth. I have a form for email and password. And on submit, it calls onSubmit method and successfully displays "onSubmit",user in the console. But the "After" is not being shown in the console. Also, the url changes to the email and password that should be used for the auth.
ex)http://localhost:3000/signin?email=aaa#gmail.com&password=abc123
Also it throws (TypeError): Cannot read property 'setState' of undefined insideonAuthStateChanged.
import React, { Component } from 'react'
import { SignUpLink } from '../SignUp';
import { Button } from 'antd';
import { connect } from 'react-redux'
import { firebase } from '../../Firebase'
const SignInPage = () => (
<div>
<h1>SignIn</h1>
<SignInForm />
<SignUpLink />
</div>
);
const INITIAL_STATE = {
email: '',
password: '',
error: null,
user: null
};
async function onSignInButton () {
console.log("Sign In Butrton")
console.log("Sign In Done")
}
const SignInButton = () => (
<Button type="primary" onClick={onSignInButton}>
Sign In
</Button>
);
class SignInFormBase extends Component {
static async getInitialProps ({ Component, ctx }) {
const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {};
return { pageProps };
}
constructor(props) {
super(props)
this.state = { ...INITIAL_STATE }
}
onSubmit = (event) => {
console.log("onSubmit", this.state)
firebase.auth.signInWithEmailAndPassword(this.state.email, this.state.password)
.catch(function (error) {
// Handle Errors here.
var errorCode = error.code
var errorMessage = error.message
console.log(errorMessage)
// ...
});
firebase.auth.onAuthStateChanged(function (user) {
if (user) {
//
this.setState({ user: user })
console.log("User", this.state.user)
} else {
// Handle error
}
})
console.log("After")
event.preventDefault();
};
onChange = event => {
this.setState({ [event.target.name]: event.target.value })
};
render () {
const { email, password, error } = this.state;
const isInvalid = password === '' || email === ''
return (
<form onSubmit={this.onSubmit}>
<input
name="email"
value={email}
onChange={this.onChange}
type="text"
placeholder="Email Address"
/>
<input
name="password"
value={password}
onChange={this.onChange}
type="password"
placeholder="Password"
/>
<button disabled={isInvalid} type="submit">
Sign In
</button>
{error && <p>{error.message}</p>}
</form>
);
}
}
Scope
this is not at the right context because you declare an anon function with the keyword function. When you do this you create a new scope. That means that the this keyword means "this function" not "this react class".
To avoid this use the arrow function () => {}
These functions pass the this along.
change this
firebase.auth.onAuthStateChanged(function (user) {
if (user) {
//
this.setState({ user: user })
console.log("User", this.state.user)
} else {
// Handle error
}
})
to this
firebase.auth.onAuthStateChanged((user) => {
if (user) {
//
this.setState({ user: user })
console.log("User", this.state.user)
} else {
// Handle error
}
})
Please note that I've already checked answers in this question and nothing seems to work.
I'm using this repo as a boilerplate. Instead of firebase database, I'm trying to send username and email with the firebase auth userid , to the node server. I created an action creator signup to handle this.
This is signup.js action creator
import * as types from '../constants/action_types';
import axios from 'axios';
export const signup = (user) => {
console.log(user);
return async dispatch => {
try {
const response = await axios.get('http://localhost:5000/api/user/register', user)
const data = await {response};
dispatch({
type : types.SIGN_UP,
payload : data.fromback
})
} catch (error) {
console.lot(error)
}
}
}
Then I've connected it with the component with mapDispatchToProps., So,under the SignUpPage component, React dev tools shows signup as a function. But when it get triggers, it gives an error saying _this2.props.signup is not a function Why's that ?
This is my SignUpPage component
import React, { Component } from 'react';
import {
Link,
withRouter,
} from 'react-router-dom';
import { auth } from '../../firebase';
import * as routes from '../../constants/routes';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {signup} from './../../actions/signup';
const SignUpPage = ({ history }) =>
<div>
<h1>SignUp</h1>
<SignUpForm history={history} />
</div>
const updateByPropertyName = (propertyName, value) => () => ({
[propertyName]: value,
});
const INITIAL_STATE = {
username: '',
email: '',
passwordOne: '',
passwordTwo: '',
error: null,
};
class SignUpForm extends Component {
constructor(props) {
super(props);
this.state = { ...INITIAL_STATE };
}
onSubmit = (event) => {
const {
username,
email,
passwordOne,
} = this.state;
const {
history,
} = this.props;
auth.doCreateUserWithEmailAndPassword(email, passwordOne)
.then(authUser => {
const userid = authUser.user.uid;
const user = { email, userid };
this.props.signup(user);
})
.catch(error => {
this.setState(updateByPropertyName('error', error));
});
event.preventDefault();
}
render() {
const {
username,
email,
passwordOne,
passwordTwo,
error,
} = this.state;
const isInvalid =
passwordOne !== passwordTwo ||
passwordOne === '' ||
username === '' ||
email === '';
return (
<form onSubmit={this.onSubmit}>
<input
value={username}
onChange={event => this.setState(updateByPropertyName('username', event.target.value))}
type="text"
placeholder="Full Name"
/>
<input
value={email}
onChange={event => this.setState(updateByPropertyName('email', event.target.value))}
type="text"
placeholder="Email Address"
/>
<input
value={passwordOne}
onChange={event => this.setState(updateByPropertyName('passwordOne', event.target.value))}
type="password"
placeholder="Password"
/>
<input
value={passwordTwo}
onChange={event => this.setState(updateByPropertyName('passwordTwo', event.target.value))}
type="password"
placeholder="Confirm Password"
/>
<button disabled={isInvalid} type="submit">
Sign Up
</button>
{ error && <p>{error.message}</p> }
</form>
);
}
}
const SignUpLink = () =>
<p>
Don't have an account?
{' '}
<Link to={routes.SIGN_UP}>Sign Up</Link>
</p>
const mapDispatchToProps = dispatch => bindActionCreators({ signup }, dispatch)
export default connect(null, mapDispatchToProps)(withRouter(SignUpPage));
export {
SignUpForm,
SignUpLink,
};
Its not a prop,
you've imported it as a function,
you can directly use it as function like this
import {signup} from './../../actions/signup';
.....
auth.doCreateUserWithEmailAndPassword(email, passwordOne)
.then(authUser => {
const userid = authUser.user.uid;
const user = { email, userid };
signup(user);
})
.catch(error => {
this.setState(updateByPropertyName('error', error));
});