This tutorial showcases a simple login example that includes a login form, a saga to query the server for authentication, redux store update for user login, which will allow access to the private pages. It will also showcase proper separation of public and private pages in React applications.
This tutorial is focused in teaching how to properly add a login page to your project. It does not contain sessions nor encription. For proper encripted login with sessions please refer to the advanced tutorial here.
The login flow will require creating the following:
The login flow has two parts. First, updating the state following the changes in email/password text fields, and second, dispatching login action once the user clicks on the login button. This tutoral focuses on the login flow itself.
The login flow begins when the user presses on the login button:
The login component does one of two things. In case the user has logged in, which is done by checking the value of isLoggedIn, the login component will redirect to /, otherwise it will render the login page. This way we ensure that the user does not login twice.
Important login component sections of code can be seen below.
const Login = (props) => {
if (props.isLoggedIn)
return (
<Redirect to='/'/>
);
else
return (
<MuiThemeProvider theme={defaultTheme}>
<div className={props.classes.loginContainer}>
.
.
.
</div>
</MuiThemeProvider>
);
};
export default withStyles(styles)(Login);
In the container we will map three login related variables:
Any change in the form data will reflect the changes on the first two parameters via two event handlers:
Once the user presses on the login button the logIn action will be dispatched. This action will be intercepted by the saga middleware, which will authenticate the values by quering the server, and dispatch the success or failure actions appropriately.
const mapStateToProps = (state, ownProps) => {
return {
isLoggedIn: state.getIn(['global', 'isLoggedIn']),
logInEmail: state.getIn(['global', 'logInEmail']),
logInPassword: state.getIn(['global', 'logInPassword']),
}
};
const mapDispatchToProps = (dispatch) => {
return {
logIn: (email, password) => {
dispatch(logInActions.logIn(email, password));
},
updateEmail: (email) => {
dispatch(logInActions.updateEmail(email));
},
updatePassword: (password) => {
dispatch(logInActions.updatePassword(password));
},
}
};
export default withTheme()(connect(mapStateToProps, mapDispatchToProps)(Login));
The login saga LoginPageSagas will intercept the action and handle it as follows:
function* logIn(action) {
console.log('login=', action);
try {
const res = yield call(fetch, action.url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(action.payload)
});
const json = yield call([res, 'json']);
if (json.response === 'OK')
yield put({type: logInConstants.LOGIN_SUCCESS, payload: json.response});
else
yield put({type: logInConstants.LOGIN_FAILURE, payload: json.response});
} catch (e) {
yield put({type: logInConstants.LOGIN_FAILURE, message: e.message});
}
}
function* LoginPageSagas() {
yield takeEvery(logInConstants.LOGIN, logIn);
}
export default LoginPageSagas;
The login reducers for the actions will update the state following dispatched actions for updating email and password values as well as login success actions dispatched from the saga. The code below shows the relevant part of the store and the handled actions for the login process.
const initialState = fromJS({
isLoggedIn: false,
logInEmail: '',
logInPassword: '',
.
.
.
});
function homePageReducer(state = initialState, action) {
switch (action.type) {
case logInConstants.LOGIN_SUCCESS:
return state.set('isLoggedIn', true);
case logInConstants.UPDATE_PASSWORD:
return state.set('logInPassword', action.payload);
case logInConstants.UPDATE_EMAIL:
return state.set('logInEmail', action.payload);
.
.
.
default:
return state;
}
};
This container maps two important variables:
const mapStateToProps = (state, ownProps) => {
return {
isLoggedIn: state.getIn(['global', 'isLoggedIn']),
component: ownProps.component,
}
};
This is the most important component in this tutorial. It enables correct routing in two cases:
The code below defines the PrivateRouter component.
const PrivateRoute = (props) => {
return (
<Route render={() => (
props.isLoggedIn
? <props.component {...props} />
: <Redirect to={{
pathname: '/login',
state: { from: props.location }
}} />
)}/>
)
};
export default PrivateRoute;
To add the login to the react application we must modify the routing rules found in our main file.
Our main router now will contain public paths only, as follows:
Inside HomePage, which also acts as a router, we route all of our application's private paths.
This ensures that our application does not display private paths prior to a successful login process.
const render = () => {
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<Switch>
<Route path="/login" component={LoginPage}/>
<PrivateRouteContainer path="/" component={HomePage}/>
<Route path="*" component={NotFoundPage}/>
</Switch>
</ConnectedRouter>
</Provider>,
document.getElementById('app')
);
};
This tutorial provided the basic details of the following elements:
Created: August 1st, 2018 Updated: August 1st, 2018