Getting started with Thorgate's SPA project template - Part 2

 

Welcome to the second part in our blog post series about Thorgate’s new SPA project template! In the first part we discussed our template a little bit and generated a new project based on the template. Now that you have a general overview of what the project structure is like, we can start building our small application.

Here are the links to the other posts:

As mentioned in the previous article, I would like to create a simple parrot reference web app that can be used to keep track of interesting parrots. Each user can add parrots and see a list of their parrots. We’ll ensure that the server-side rendering works and make use of some of the interesting technologies included in the project template.

Showing parrots

Let’s start with showing the user their saved parrots. Since we already have users and authentication set up, we don’t need to put too much effort into getting the current user’s personal parrots.

Firstly, it’s a good idea to create a superuser account for development. We would normally call python manage.py createsuperuser to create a superuser in Django, but since we’re running everything inside Docker, then the command would be a bit longer. Something like this would do the trick: docker-compose run --rm django python manage.py createsuperuser. But who can remember that? In order to make it easier for everyone, the Makefile includes a useful alias for running management commands: docker-manage. We can create a new superuser like this: make docker-manage cmd="createsuperuser". Now we can either click on “Admin panel” in the navbar or go to http://127.0.0.1:8000/adminpanel/ to see the Django admin page and log in.

Setting up models and Django admin

We need to stick our parrot-management logic somewhere, so let’s create a new app: parrots.

$ make docker-manage cmd="startapp parrots"

Since the folder is created inside the Docker container, it is possible that its permissions are incorrect. We can fix that by running the following command:

$ sudo chown -R "$(id -un):$(id -gn)" parrot_mania

We will also need to add our newly generated app to our INSTALLED_APPS in settings/base.py:

# parrot_mania/settings/base.py

INSTALLED_APPS = [
    # Local apps
    'accounts',
    'parrot_mania',
    'parrots',
    ...
]

Now we can add our Parrot model in parrots/models.py:

# parrot_mania/parrots/models.py

from django.db import models
from accounts.models import User

class Parrot(models.Model):
    name = models.CharField(max_length=255)
    link = models.TextField()
    user = models.ForeignKey(User)

And let’s register the Parrot model to our admin site in parrots/admin.py as well:

# parrot_mania/parrots/admin.py

from django.contrib import admin
from parrots.models import Parrot

admin.site.register(Parrot)

And let’s migrate our database using aliases from the Makefile. Use the following commands in your terminal:

$ make makemigrations
$ make migrate

Now we can add parrots through the Django admin.

Creating a parrot in Django admin
Creating a parrot in Django admin

Showing hard-coded parrots in the front-end

The next step is to create a page on the front-end where we will show the parrots. At first, let’s use some dummy data so that we can focus on just the React piece of the puzzle. Create a file at app/src/views/ParrotsList.js:

# app/src/views/ParrotsList.js

import React from 'react';
import { Container } from 'reactstrap';

import withView from 'decorators/withView';

const exampleParrots = [
    { id: 1, name: 'Alex', link: 'https://www.youtube.com/user/doorbell26' },
    { id: 2, name: 'Benjamin', link: 'https://www.youtube.com/user/parrotpost' },
];

const ParrotsList = () => (
    <Container>
        <h1>Here are your parrots!</h1>
        <ul>
            {exampleParrots.map((parrot) => (
                <li key={parrot.id}>
                    <a href={parrot.link}>{parrot.name}</a>
                </li>
            ))}
        </ul>
    </Container>
);

export default withView()(ParrotsList);

But how do we specify that this is an actual page that React should render on a specific URL? Well, we need to specify this in the src/configuration/routes.js file.

Add the following object toward the end of the existing routes, just above the last NotFoundRoute:

# app/src/configuration/routes.js

...
{
    path: '/parrots',
    exact: true,
    name: 'parrots-list',
    component: ParrotsList,
},
NotFoundRoute,

We also need to import the component though, so let’s add this to the top of the file:

# app/src/configuration/routes.js

const ParrotsList = loadable(() => import('views/ParrotsList'));

This funny import handles code-splitting, dynamically loading only the necessary JavaScript for the page that the user is currently on.

Now we can navigate to http://127.0.0.1:8000/parrots and see our example parrots.

Example hard-coded parrots
Example hard-coded parrots

This is a good time to ensure that our server-side rendering works, we can curl the /parrots page or right click and “View page source”:

$ curl http://127.0.0.1:8000/parrots

...
<h1>Here are your parrots!</h1>
...

Even though the HTML is minified, we can see that it includes our example parrots and their links.

API for parrots using Django Rest Framework

So far it has been a pretty standard Django and React application, but now that we need to show dynamic data to the user, we need communication between the server and the client. Let’s first do this via standard HTTP requests and then bring in Redux Saga later.

Firstly, we need an API endpoint to fetch parrots. Using Django Rest Framework, it is possible to set up API endpoints very quickly. Let’s create our DRF serializer in parrot_mania/parrots/serializers.py:

# parrot_mania/parrots/serializers.py

from rest_framework import serializers

from parrots.models import Parrot

class ParrotSerializer(serializers.ModelSerializer):
    class Meta:
        model = Parrot
        fields = ('id', 'name', 'link')

And our viewset in parrot_mania/parrots/views.py:

# parrot_mania/parrots/views.py

from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated

from parrots.models import Parrot
from parrots.serializers import ParrotSerializer

class ParrotViewSet(viewsets.ModelViewSet):
    queryset = Parrot.objects.all()
    serializer_class = ParrotSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        """Return the authenticated user's parrots."""
        return super().get_queryset().filter(user=self.request.user)

And register the viewset’s URLs using Django Rest Framework’s router in
parrot_mania/parrots/urls.py:

# parrot_mania/parrots/urls.py

from rest_framework import routers

from parrots.views import ParrotViewSet

router = routers.SimpleRouter()
router.register(r'parrots', ParrotViewSet, base_name='parrots')
urlpatterns = router.urls

And finally include these URLs in parrot_mania/parrot_mania/rest/urls.py:

# parrot_mania/parrot_mania/rest/urls.py

urlpatterns = [
    ...
    url(r'^', include('parrots.urls')),
]

We can test it out with curl if we temporarily remove the authentication check and current user filtering:

# parrot_mania/parrots/views.py

class ParrotViewSet(viewsets.ModelViewSet):
    queryset = Parrot.objects.all()
    serializer_class = ParrotSerializer
    # permission_classes = [IsAuthenticated]

    # def get_queryset(self):
    #     return super().get_queryset().filter(user=self.request.user)

And the curl command:

$ curl http://127.0.0.1:8000/api/parrots/

[
    {"id":1,"name":"Alex","link":"https://www.youtube.com/user/doorbell26"},
    {"id":2,"name":"Benjamin","link":"https://www.youtube.com/user/parrotpost"}
]

Fetching the parrots in the front-end

We need to get the data about parrots to our front-end though. At first, let’s try to do this with a simple fetch request:

# app/src/views/ParrotsList.js

import React, { Component } from 'react';
import { Container } from 'reactstrap';

import withView from 'decorators/withView';

class ParrotsList extends Component {
    state = {
        parrots: [],
    };

    componentDidMount() {
        fetch('/api/parrots')
            .then((res) => res.json())
            .then((parrots) => {
                this.setState({ parrots });
            });
    }

    render() {
        return (
            <Container>
                <h1>Here are your parrots!</h1>
                <ul>
                    {this.state.parrots.map((parrot) => (
                        <li key={parrot.id}>
                            <a href={parrot.link}>{parrot.name}</a>
                        </li>
                    ))}
                </ul>
            </Container>
        );
    }
}

export default withView()(ParrotsList);

The problem here, is that the parrots no longer get server-rendered as they are fetched in componentDidMount — when the component has been rendered on the client side. In order to fix this, you can write your first saga. A saga listens to any dispatched Redux actions and can react to them — for example, dispatching more actions or making API requests. In our SPA projects, all communication with the API goes through sagas.

The saga we will write will be very simple. It’s going to be called whenever anyone navigates to the /parrots route. In addition, it will be called on the server whenever the user initially navigates to the /parrots route.

At first, let’s just log something to the console so that we see that it’s working. Create an app/src/sagas/parrots directory and a fetchUserParrots.js file inside it with the following contents (ensure not to miss the * next to function as it’s a generator function):

# app/src/sagas/parrots/fetchUserParrots.js

export default function* fetchUserParrots() {
    console.log('Fetching some parrots!');
}

We also need to configure this saga to be triggered whenever users visit the /parrots page. In order to do that, we can import the saga and modify our route configuration in configuration/routes.js:

# app/src/configuration/routes.js

import fetchUserParrots from 'sagas/parrots/fetchUserParrots';

...
{
    path: '/parrots',
    exact: true,
    name: 'parrots-list',
    component: ParrotsList,
    initial: [
        fetchUserParrots,
    ],
},

Now we should see the message logged in the console.

Sagas offer a way of handling side effects in Redux projects (like API requests) that are easy to manage and test. Redux Saga has a nice introduction on their website at https://redux-saga.js.org.

In our project template, the sagas specified in the initial array for each route are executed whenever a user visits the route. They’re also executed on the server-side so that data can be fetched on the server and returned to the user when they first query the page.

Now that we have a function that’s triggered when the user queries /parrots, we can add API request logic there. But before that, let’s create a Redux duck to contain the parrots. Create the app/src/ducks/parrots.js file and add the following:

# app/src/ducks/parrots.js

import { combineReducers } from 'redux';

export const RECEIVE_PARROTS = 'parrots/RECEIVE_PARROTS';

const parrotsReducer = (state = [], action) => {
    switch (action.type) {
        case RECEIVE_PARROTS:
            return action.parrots;

        default:
            return state;
    }
};

export default combineReducers({
    parrots: parrotsReducer,
});

// Action creators

export const receiveParrots = (parrots) => ({
    type: RECEIVE_PARROTS,
    parrots,
});

We also need to configure our application to use the parrots duck. We can do that in configuration/reducers.js:

# app/src/configuration/reducers.js

import parrots from 'ducks/parrots';

export default (history) => combineReducers({
    ...
    parrots,
});

Finally, we can connect the ParrotsList component to the Redux store:

# app/src/views/ParrotsList.js

import React from 'react';
import { Container } from 'reactstrap';
import { connect } from 'react-redux';

import withView from 'decorators/withView';

const ParrotsList = ({ parrots }) => (
    <Container>
        <h1>Here are your parrots!</h1>
        <ul>
            {parrots.map((parrot) => (
                <li key={parrot.id}>
                    <a href={parrot.link}>{parrot.name}</a>
                </li>
            ))}
        </ul>
    </Container>
);

const mapStateToProps = (state) => ({
    parrots: state.parrots.parrots,
});

const ParrotsListConnector = connect(
    mapStateToProps,
)(ParrotsList);

export default withView()(ParrotsListConnector);

It is now possible to dispatch RECEIVE_PARROTS actions with some parrots and view them. Let’s quickly try this out without any API requests just to ensure everything works alright. Edit the fetchUserParrots saga:

# app/src/sagas/parrots/fetchUserParrots.js

import { put } from 'redux-saga/effects';

import { receiveParrots } from 'ducks/parrots';

export default function* fetchUserParrots() {
    yield put(receiveParrots([
        { id: 1, name: 'Alex', link: 'https://www.youtube.com/user/doorbell26' },
        { id: 2, name: 'Benjamin', link: 'https://www.youtube.com/user/parrotpost' },
    ]));
}

And we should now be able to see the hard-coded parrots again. We dispatch the RECEIVE_PARROTS action using the put function from Redux Saga. The next thing to do is to make an API request to retrieve the user’s parrots and dispatch the action using those parrots instead.

We could use fetch to make this API request, but we have a helper library for making API requests called tg-resources (which uses fetch or superagent under the hood). Using tg-resources, we can configure a “resource” that can be used to easily make API calls. Let’s configure the parrotsList resource in services/api.js. In the createSagaRouter arguments, add the parrots resource like so:

# app/src/services/api.js

const api = createSagaRouter({
    ...
    parrots: {
        list: 'parrots/',
    },
}, {
    ...headers, apiRoot settings, etc.
});

We can now make use of this resource in the fetchUserParrots Saga:

# app/src/sagas/parrots/fetchUserParrots.js

import { put } from 'redux-saga/effects';

import { receiveParrots } from 'ducks/parrots';
import api from 'services/api';

export default function* fetchUserParrots() {
    try {
        const parrots = yield api.parrots.list.fetch();
        yield put(receiveParrots(parrots));
    } catch (err) {
        console.error('Something went wrong!');
    }
}

We call the fetch method on the api.parrots.list resource triggering a GET request to /api/parrots/. Similarly, we could use post to make a POST request with some data.

The saved parrots should now show up on the page and in the HTML that the server sends to the client. This can be double checked by right clicking on the page and selecting “View page source”.

I created a simple graphic to show how server-side rendering works in the /parrots view:

Server-side rendering of /parrots
Server-side rendering of /parrots

Server-side rendering allows search engine crawlers to index pages while we still write all rendering using React.

Next steps

In this post we showed the user a list of their parrots. The parrots can only be added by superusers in Django admin though, so let’s add a more convenient way to save parrots in the next blog post. We'll be using Formik and Redux Saga to create a form for adding parrots and to send the request to create a parrot to the server.

This has been the second article in a series of blog posts about Thorgate’s new SPA project template. Check out the next post here: