Login - Part 1
In this video we are going to start building up our front-end Login functionality. It is important to point out that to follow along, you must have some form of working API.
Now, the good news is that if you do not have a working API, then you can use the same one that is used throughout this series. It's made in Symfony 3, using FOSUserBundle, FOSRESTBundle, and LexikJWTAuthenticationBundle. Oh boy, that's a lot of bundles.
If you are the kind of maverik who sees code like Neo sees the Matrix, then you can grab the API code right here. If you aren't yet sure what any of this means, I'd advise you to first follow along with the course that walks through this code, where you will learn how the back end API is put together.
Once you have the API online, in our case, we are going to run the Behat tests to ensure that the Login functionality is working as expected:
Feature: Handle user login via the RESTful API
In order to allow secure access to the system
As a client software developer
I need to be able to let users log in and out
Background:
Given there are Users with the following details:
| id | username | email | password |
| 1 | peter | peter@test.com | testpass |
| 2 | john | john@test.org | johnpass |
| 3 | tim | tim@blah.net | timpass |
And I set header "Content-Type" with value "application/json"
Scenario: User can Login with good credentials (username)
When I send a "POST" request to "/login" with body:
"""
{
"username": "peter",
"password": "testpass"
}
"""
Then the response code should be 200
And the response should contain "token"
This is the Behat feature spec for our Login flow. If the test passes, it proves that we can login to our API by sending in JSON in the format:
{
"username": "peter",
"password": "testpass"
}
And we should expect a response from our API with a 200
status code, and a body containing a token. This token will be a JSON Web Token, or JWT (pronounced jot). We will need to use this token for any subsequent requests to our API, for example when fetching our user profile information.
Planning Our Login Form Component
Disregarding the fact that our form is about 'Login', we essentially need to create a component that is a form.
Our form component will contain all the fields - the username
, and password
input fields - and also the submit
button. Where things become a little more confusing is that this component won't actually know what happens when the form is submitted.
Instead, it relies on another component to pass in the function to call when the form is submitted. This function to call on form submit is passed in as a prop
.
The login form isn't the best example of why this is useful. Instead, maybe think about your user profile.
In the profile we may have your age
, email
, and bio
fields.
This form is going to be the same whether you want to add your profile data for the first time, or edit your existing profile data.
However, what happens when you submit this form will differ, depending on whether this is the first time you are submitting, or if this is an update. In the case of a new profile, we may want to POST
the data. In the case of an update, a PUT
, or PATCH
would be a better choice.
Therefore, we could re-use the same form, but have two different 'wrappers'. I don't think this is set in stone, but it is very common to see these 'wrapper' components referred to as 'Containers'.
In our profile example, we may have a AddProfileContainer
, and an EditProfileContainer
. Both would look extremely similar, importing the Login Form component, and defining an onSbumit
function. Where they would differ is in the implementation of this onSubmit
function - the AddProfileContainer
calling a POST
, whereas the EditProfileContainer
may call a PUT
or PATCH
.
Now, that's not strictly true anyway, as they wouldn't directly call the API - instead, they would delegate to a saga, but more on that shortly.
Anyway, after all this we can deduce that our LoginForm
component is going to need some sort of LoginFormContainer
to contain the function to call on submit, and so on. Instead of calling this a LoginFormContainer
, it makes more sense to simply call this the LoginPage
, but it will live inside the src/containers
directory.
Updating The State With Form Data
It's hard to get through a web project without having at least one form in your project. Most projects have several, and many projects are entirely about forms.
It seems odd, therefore, that working with forms in React can be quite painful.
That I am aware of, there are two approaches to working with forms.
You can go with simple forms, which have no logic to keeping the state updated as the user types / changes inputs. These forms only care about updating the state whenever the user finally submits the form. Again, these are simple, but certain features such as validation as you type are not easy to implement.
On the opposite side to this are more complicated forms, whereby each field updates the state as you type / change inputs. This is great for e.g. the on-demand validation, but adds complexity in terms of keeping the state in sync with the form field values at all times.
Redux Form aims to solve this problem, and largely does a very good job of it in the process. The only downside? It can be quite complicated to understand just what is happening under the surface. One part in particular - the passing of the submit function - is particularly confusing, in my opinion.
Ultimately it is a trade off - and for me, Redux Form wins out.
Implementing Our Login Form Component
Knowing the above, let's now look at the code from the LoginForm
component.
// src/components/LoginForm.js
import React from 'react';
import {Field, reduxForm} from 'redux-form';
const LoginForm = () => {
return (
<form>
<Field component="input"
name="username"
id="username"
type="text"
placeholder="Username or email address"
required="required"
/>
<Field component="input"
name="password"
id="password"
type="password"
placeholder="Password"
required="required"
/>
<button type="submit">
Login
</button>
</form>
);
};
export default reduxForm({
form: 'login'
})(LoginForm);
Whenever we work with React, we always start by importing react
.
Next, we are going to import the Field
component from redux-form
. This is how we connect the user's input to our Redux store. You can dive further in to the Field
component in the Redux Form docs. The important parts for us are the props
of component
, and name
.
In our case we are using the simple string value of input
for our component
, which renders a text input. Other choices here are select
and textarea
. You can, of course, get much fancier with your form fields, but we need not worry about that at this stage.
Our fields must be named, and the names themselves are self explanitory.
All other information is optional, and is simply passed as props to the element generated by the component
prop - the input
element, in our case.
The submit button is standard HTML. Nothing fancy here.
To hook this form up to Redux Form, we must wrap the LoginForm
component inside the reduxForm
higher order component. In other words, the reduxForm
function is going to be called with our form is its parameter, and an object telling the function what this form is called - login
in this case. This name has to be unique throughout our project.
You can pass other information into this object, such as validation rules, initial field values, or even define the function to call when this form is submitted. But we aren't doing any of that at this stage.
If you were to use this form now, it wouldn't actually submit as expected. We have yet to set up the onSubmit
function.
What you would notice though is that for every interaction you have with the form, Redux Form is going to dispatch an action describing the change you just made. This can be as simple as putting your cursor into a field and clicking - giving focus to the field - or more complex, in so much as passing in the currently field value to be set as the new state for that field. This will unfortunately fill up your console log very quickly with redux-logger messages.
However, there is another issue - you may notice that you can type, but the characters you type never show in the field itself. This is because we haven't hooked up the Redux Form reducer. Let's fix that:
// /src/reducers/index.js
import { combineReducers } from 'redux';
import {routerReducer} from 'react-router-redux';
import {reducer as formReducer} from 'redux-form';
const rootReducer = combineReducers({
form: formReducer,
routing: routerReducer
});
export default rootReducer;
Simple enough - import the formReducer
, and include it in the object consumed by combineReducers
.
Now our form fields should work as expected. Only, when the form is submitted, it is still not working. Let's fix that also.
// /src/containers/LoginPage.js
import React, {Component} from 'react';
import LoginForm from '../components/LoginForm';
class LoginPage extends Component {
doLogin(formData) {
console.log('form data was received', formData);
}
render() {
return (
<LoginForm
onSubmit={this.doLogin.bind(this)}
/>
);
}
}
export default LoginPage;
Here, we have started by import
'ing React.
Then, we import
the LoginForm
. Note that we are in the /src/containers
directory, so need to go up a directory, and into the components
directory to get the LoginForm
.
Rather than making this a stateless function, instead our LoginPage
is a class which extends React.Component
.
Our render
method outputs the form, but critically, it passes in the onSubmit
prop, which references the doLogin
function inside this class.
We must also update the LoginForm
component to accept this onSubmit
prop, and then use it when the form is submitted:
// /src/components/LoginForm.js
import React from 'react';
import {Field, reduxForm} from 'redux-form';
const LoginForm = (props) => {
return (
<form onSubmit={props.handleSubmit}>
<Field component="input"
name="username"
id="username"
type="text"
placeholder="Username or email address"
required="required"
/>
<Field component="input"
name="password"
id="password"
type="password"
placeholder="Password"
required="required"
/>
<button type="submit">
Login
</button>
</form>
);
};
LoginForm.propTypes = {
onSubmit: React.PropTypes.func.isRequired
};
export default reduxForm({
form: 'login'
})(LoginForm);
Note the additions here.
Firstly we now pass in props
to our LoginForm
function.
Next, we define an onSubmit
event listener, and use the value of props.handleSubmit
. This is confusing, as didn't we pass in a prop
called onSubmit
?
Why, yes we did. More on this in a second.
For now, let's skip that and cover that because we have defined a prop
called onSubmit
, and passed it in, we should update the LoginForm.propTypes
to serve as extremely useful documentation to ourselves, and any other developers who use this component as to what props
are expected, and how they should behave. In this case, our onSubmit
prop is expected to be a func
(function), and is required.
Ok, but coming back to this onSubmit
/ handleSubmit
thing. I have to say, I found this entirely confusing. I have scoured the documentation, GitHub tickets, and several pages of Google for various searches and I haven't found documentation that explained this succinctly.
As best I understand it, the props.handleSubmit
function is defined by Redux Form.
Redux Form's props.handleSubmit
function will expect us to pass in our own function as a prop
called props.onSubmit
.
By following this convention, we don't need to explicitly call props.handleSubmit(props.onSubmit)
, which instead happens implicitly.
I'm still not sure I understand this fully, but that's how the code reads to me. Feel free to correct me if I have this wrong.
Anyway, this works, and if you submit the form now, you should see the console log statement showing whatever form data you just submit.
This is cool as this now enables us to proceed further, using this form data to send to our Redux Saga which will handle the process of login for us. This is what we will move on to in the very next video.