Frontend React: Social Media

Login and register

Time to start working on some actual interesting stuff! For starters, we need to allow users to create an account and then log in into their account. Most of our application will be behind an authentication wall (unauthenticated users will be restricted from most of the app).

Creating the login form

We need to create a new component in our components folder (I suggest grouping them by their type). We will need a LoginForm component (I would suggest to place it in the src/components/forms/LoginForm.js file so we can have all the forms grouped together in the project).

For the form, we need two fields: username and password. The input fields should have their appropriate input types, and these fields should be uncontrolled (they have to have their value and onChange props set).

When the user will submit the form (either by clicking on "Log in" or by pressing the "Enter" key), the credentials will need to get sent to the server. For now, let's just `console.log` them in a single object, so we can see that the right information would be sent.

A request-response cycle in the world of frontend usually has these states:
building the request -> loading -> error or successful.

Our form should be able to:

  • Display a loading state
  • Display any errors that came from the server. The server will respond with errors in the following format:
    • {"email": ["already_taken"], "password": ["too_short"]} - when there are validation errors for certain fields, a JSON response will be returned, where each key is the name of the field that failed the validation, and the value is a list of error messages.
    • {"non_field_errors": ["some message"]} - when there are some errors that can not be tied directly to a certain field (eg. wrong username or password).
    • {"detail": "..."} - Generic error messages

When the sends an error for a form that includes the field names (the first case), we should display the error messages under the field that failed that validation. For example, if the server reports that the email is already taken, ({"email": ["already_taken"]}) we should display that message under the email field.

Sending the credentials

To send the credentials to the server, we need to make a POST request to our backend, at the/api/v1/login endpoint, with the username and password. We will receive a tokenthat we need to store somehow on the frontend (usually in the localStorage).

If there are errors, we should display the errors accordingly (if field level errors are reported, display them under their own fields, otherwise, display them in a generic Alert

To do the request, we should use the fetch function.

Authenticated views and logging off

Once we have the token, we should do something with it.

We need to create two kinds of views (general page wrappers):

  • AnonymousRoute that will render its content no matter what
  • AuthenticatedRoute that will render its content only when a valid token is present in the localStorage

The AnonymousRoute is easy to implement, so we won't insist on it. But for the AuthenticatedRoute, we need to have the following rendering flow:

  • When the component mounts, we need to fire a request to fetch the current user (/api/v1/users/me) using the token we have stored (send it in the Authentication HTTP header, with the value
    Token <token value here>. If there is no token stored, it is considered an anonymous user.
  • If the response from the server doesn't contain the user information and it contains an error instead (hint: the status code offers us that information), the token is invalid. It must be deleted from localStorage.
  • For these two cases reported above (missing or invalid token), the user needs to be redirected to the login screen.
  • If the response is alright, we can continue to display the route as usual.

Next, we need to convert the NewsfeedPage to be served through an AuthenticatedRoute. You should update that page to include a navigation bar, in which we should add a Log out link.

Contexts

One more thing, that we need to do: expose the user object to the whole application, using a react context.

We should create a UserContext, and have the AuthenticatedRoute use that to pass the user object downstream and allow any children from its hierarchy to access the user object.

Logging off

When the user will press the Log out link from any authenticated route, we need to delete the token from localStorage and then redirect the user to an anonymous route (if you have a farewell screen, you should redirect the user there, otherwise, redirecting them to the login screen is a fine solution too).

Register

The next step is to create a user registration flow. This one should be simpler than the registration flow.

To create a user, we need to make a POST request to the /api/v1/users/register endpoint. Don't forget that you can always check out the http://localhost:8000/docs/swagger/ link for the API documentation for the backend.

The registration form should have the following fields:

  • username
  • email
  • password
  • first name
  • last name

We should ignore the avatar for now. Uploading files requires some knowledge about the multipart/form-data encoding, which is a more advanced topic, with which we will deal later.

The rules from above still apply (error handling, loading state).

After the user is created successfully, we need to instantly log in the user, but we don't get a token from the registration endpoint. We can work around that because we should have the username and password stored in some state. After the user is successfully created, we can immediately make a login request afterwards to get the token, then we follow the login flow as usual.

Writing unit tests

As every developer that respects himself/herself, our code needs to contain unit tests.

Some developers will say "yeah but I don't write buggy code". And even if you don't ever make a mistake in your code (which is never the case because I assure you, everybody makes mistakes), we write unit tests to prevent the code to break in the future. As the project grows in size, our code base will become larger, our components will start reusing code from other parts, etc.

When we change something, there's no way to tell easily if the change will impact something in the application.

Writing unit tests helps us not mess up the project in the future.

In the React ecosystem, testing is done mostly using jest and @testing-library/react. You can read more about this combo here: https://create-react-app.dev/docs/running-tests/

As part of this task, you should write some tests for your two forms:

  • Inputting data and submitting results in the correct information being sent to server
  • Rendering the form with field errors results in correct placement
  • Rendering the form with general errors results in correct rendering
  • The loading state is correctly handled