Pixies, part11: Connect the Frontend to the Backend


Where at are we now

All right, at this point we have containerized the frontend, the backed and also, for the development purposes, we are using the Postgres container. Sadly, our frontend uses hardcoded data, not the data which the backend is happy to serve us. In this module we are going to fix that. But first, let’s makes sure we have our containers running. In the backend directory do:

docker-compose up -d

Technically, at this stage we are going to use the React app in the development mode, so we are not going to use the frontend container until the end of the module. But since the frontend container and the React app in dev mode listen on different ports, they are going to co-exist just fine. If you want, you can stop the frontend container, but that is not necessary.

Ok, let’s switch gears and go to the frontend directory.

I usually start the React app in the development mode to take advantage of the live reloads, so I see the changes in the app pretty much instantly once I save the source file.

npm start

In the App.js we have this array of hardcoded items. We want to replace it with the data dynamically loaded from the backend. To make it work we have to utilize React hooks

React hooks

React hook is a special function that let’s you hook into React features, as they say in the documentation. What does it mean though?

React state

In your frontend application you have some data which you want React to display. In our case it is items array. Also you want React to re-display the relevant portions of the page once your state changes, for example, you replace initial items with the ones downloaded from the backend. React’s ‘useStatehook helps us with that. Rename theitemstoinitialItemsand apply theuseState hook:

// changed
import React, { useState } from 'react';
// ...
const App = () => {
    // changed.
    const intitialItems = [
        // ...
    ];
}
// new
const [items, setItems] = useState(initialItems);
// ...

If you check your app, nothing seems really to change. As before, the carousel populates the elements from the items array. However notice that now we have the call to useState, This takes, as the parameter, the object we want to initialize our state to, and returns back two things, the state itself (items) and the function we can call when we want to change the state (setItems). React is designed the way that when this function is called, it will react to the change (hence the name of the library) and re-display the relevant parts of the page, name the ones which use items. So our plan is to call the backend, retrieve the list of photos from it and pass it as an argument to the setItems. Now, what would be a good time to call the backend. Most likely, when the App component is loaded, we use another React hook for that.

useEffect hook

The useEffect hook is being balled by React when the component is loaded. Let’s see how it is done.

// changed
import React, { Fragment, useState, useEffect } from 'react';
// ...
const App = () => {
    // new
    useEffect(() => {
        console.log('component is loaded!);
    }[]);
}

useEffect takes two parameters, the function to be called when the component is loaded and a list of dependencies which we can keep empty for our purposes. Once the page is reloaded, you will see that our debug output is printed to the javascript console. So the useEffect hook works as expected.

Great, now we are need instead of our debug output to retrieve the list of items from the backend

axios

We are going to use axios, very popular and easy-to use javascript library for making http calls so we can reach our backend Rest API. Let us install it

npm install axios --save-dev

The backend must be running by now as the Docker container. Just to be sure see if you can reach http://localhost:9090/api/ in the browser. Temporarily, we are going to hardcode this URL in the App.js

const App = () => {
  useEffect(() => {
    // new
    const url = 'http://localhost:9090/api/';

Now, let axios to make the http call. It is an async call and we will have to specify what to do when it succeeds and when it fails. In the case of failure, we just log an error to the javascript console, If it succeeds, we call the setItems passing whatever is delivered from the backend as an argument. If everything goes well, our React app is expected to react to the state change and replace the relevant parts of the page.

// ...
// new
import Axios from 'axios';
// ...
useEffect(() => {
    const url = 'http://localhost:9090/api/';
    Axios.get(url)
      .then(response => {
        setItems(response.data);
      })
      .catch(error => {
        console.log(error);
      });
  }, []);

Reload the page. Hm, nothing has changed, we still see our initialPhotos. However, if we go to the javascript console, we are going to see the error:

Access to XMLHttpRequest at 'http://localhost:9090/api/' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Actual text may vary, depending on the browser, but clearly it complains about something called CORS

CORS

By default, servers implement the same-origin policy which allows the browser apps to access its resources only if they are served from the same URL. Since our setup is using different ports for the frontend and the backend, their URLs are considered different and hence the error. The CORS protocol (Cross-Origin Resource Sharing) specifies the way the backend can relax this policy by maintaining the whitelist of the frontend addresses which are authorized. So it seems, we will need to tweak our backend. Let’s go to the backend directory and install the package which helps us to maintain the list of safe frontend addresses

pip install django-cors-headers

This package is also a Django app, so we need to add it to the list of INSTALLED_APPS, add coresponding MIDDLEWARE and configure, as usual, through the environment variables.

INSTALLED_APPS = [
    // ...
    // new
    "corsheaders",
]
// ...
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
# new
    "corsheaders.middleware.CorsMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
# ...
]
// ...
CORS_ORIGIN_WHITELIST = env.list("CORS_ORIGIN_WHITELIST", default=[])

It is a good practice to default to the empty list and explicitly opt-in the addresses we want to allow. Obviously, in our docker-compose.yml, we must add our frontend address to the whitelist.

# ...
backend:
    build: .
    environment:
      - PORT=80
      - SECRET_KEY=hushush
      - DATABASE_URL=postgresql://pixies:pixies@db:5432/pixies
      - ALLOWED_HOSTS=*
# new
      - CORS_ORIGIN_WHITELIST=http://localhost:8080,http://localhost:9091

We are adding our the addresses for our the frontend being served in the dev mode and also for the frontend running inside the container. Before we rebuild the backend image, let’s update the dependencies

pip freeze > requirements.txt

Now let’s rebuild and restart the containers

docker-compose up -d --build

When you browse to http://localhost:9091 and refresh the page you should see that the data about photos is retrieved from your backend. If you want to edit the items, you can go to http:9090/admin

So our frontend is able to connect to the backend and retrieve the data from there. One problem though. The backend’s URL is hardcoded in the frontend’s code. Ideally, we would like to configure it too, through the environment variables. We are going to do that in the next module.


See also