covers-Blog_post_header_optimized_JQffROq.png

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

author

Written by Joosep

April 08, 2019 | 5 min read

This is part 3 of our blog post series about Thorgate’s new SPA project template. So far, we have generated a new project and implemented showing parrots to the user. We used Redux Saga to retrieve the parrots and Django Rest Framework for creating the API.

Here are the links to the other posts:

In this article we’ll implement adding parrots using Formik and Redux Saga.

Creating parrots

Currently, the only way to add parrots is via the admin page. This is a bit of a problem since then only admin users can add parrots. Also, it’s quite inconvenient to navigate to the “add a parrot” admin page. So, let’s create a new view with a form for creating parrots.

Let’s add a simple non-functional form view to views/CreateParrot.js:

# app/src/views/CreateParrot.js

import React from 'react';
import { Form, FormGroup, Label, Input, Button, Container } from 'reactstrap';

const CreateParrot = () => (
    <Container>
        <h1>Add a new parrot!</h1>
        <Form>
            <FormGroup>
                <Label for="name">Name</Label>
                <Input name="name" id="name" />
            </FormGroup>
            <FormGroup>
                <Label for="link">Link</Label>
                <Input name="link" id="link" type="url" />
            </FormGroup>
            <Button>Create</Button>
        </Form>
    </Container>
);

export default CreateParrot;

And add a route for it in configuration/routes.js:

# app/src/configuration/routes.js

const CreateParrot = loadable(() => import('views/CreateParrot'));
...
{
    path: '/parrots/create',
    exact: true,
    name: 'create-parrots',
    component: CreateParrot,
},

You should see a page like this at /parrots/create:

Form for creating a parrot

Unfortunately, we don’t currently have access to any field values that the user inputs. One option would be to convert the component to a class component and keep track of the name and the link in the component’s state. Another option would be to use refs and manually query the name and link inputs for their values when the user submits the form. Hooks could also be used instead of converting the component to a class component.

Those solutions require a lot of manual work though (especially for larger forms). So, let’s use a package that makes forms in React a breeze: Formik. We can convert our component to use Formik like so:

# app/src/views/CreateParrot.js

import React from 'react';
import { FormGroup, Label, Input, Button, Container } from 'reactstrap';
import { Formik, Form } from 'formik';

const CreateParrot = () => (
    <Container>
        <h1>Add a new parrot!</h1>
        <Formik
            initialValues={{ name: '', link: '' }}
            onSubmit={(values) => {
                console.log('Submitting!', values);
            }}
        >
            {({ values, handleChange }) => (
                <Form>
                    <FormGroup>
                        <Label for="name">Name</Label>
                        <Input
                            name="name"
                            id="name"
                            value={values.name}
                            onChange={handleChange}
                        />
                    </FormGroup>
                    <FormGroup>
                        <Label for="link">Link</Label>
                        <Input
                            name="link"
                            id="link"
                            type="url"
                            value={values.link}
                            onChange={handleChange}
                        />
                    </FormGroup>
                    <Button type="submit">Create</Button>
                </Form>
            )}
        </Formik>
    </Container>
);

export default CreateParrot;

Formik keeps track of the form’s state internally and uses a render prop-based API to allow us to access the state and modify it. It is simple to add validation, show validation errors and handle server errors. Even things like disabling the submit button when the user has clicked it is quite straight-forward.

You should now see a message logged to the console with the values you inputted.

Now, we need to send this data to the server so that the server can store the parrot in the database. A very simple way to do it would be to just send a request using fetch in the onSubmit handler, but let’s use a saga for that.

Let’s add a createParrot.js file to sagas/parrots/ with the following contents:

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

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

const CREATE_PARROT = 'CREATE_PARROT';

export const createParrot = (name, link) => ({
    type: CREATE_PARROT,
    name,
    link,
});

function* createParrotSaga(createParrotAction) {
    console.log('create parrot:', createParrotAction);
}

export default function* createParrotWatcher() {
    yield takeEvery(CREATE_PARROT, createParrotSaga);
}

This saga specifies its own action that it can react to — CREATE_PARROT. The createParrotWatcher will be watching for any dispatched CREATE_PARROT actions and run createParrotSaga passing it the action as the argument. Currently we’re just logging the createParrotAction in order to ensure that we have the correct data available. Let’s configure this Saga to “watch” for any actions dispatched in the /parrots/create route by adding it as the watcher for that route in routes.js:

# app/src/configuration/routes.js

import createParrotWatcher from 'sagas/parrots/createParrot';
...
{
    path: '/parrots/create',
    exact: true,
    name: 'create-parrots',
    component: CreateParrot,
    watcher: createParrotWatcher,
},

And finally, let’s connect our CreateParrot component to Redux:

# app/src/views/CreateParrot.js

...
import { connect } from 'react-redux';

import { createParrot } from 'sagas/parrots/createParrot';

const CreateParrot = ({ createParrot }) => (
    <Container>
        <h1>Add a new parrot!</h1>
        <Formik
            initialValues={{ name: '', link: '' }}
            onSubmit={(values) => {
                createParrot(values.name, values.link);
            }}
        >
            ...
        </Formik>
    </Container>
);

const mapDispatchToProps = (dispatch) => ({
    createParrot: (name, link) => dispatch(createParrot(name, link)),
});

const CreateParrotConnector = connect(
    null,
    mapDispatchToProps,
)(CreateParrot);

export default CreateParrotConnector;

Now, whenever the form is submitted, we dispatch the CREATE_PARROT action passing it the name and the link.

We should now see the message logged in the console with the createParrotAction including the specified name and link.

We can now send a POST request to the server using our parrots.list resource:

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

...
import api from 'services/api';

...

function* createParrotSaga(createParrotAction) {
    try {
        const { name, link } = createParrotAction;
        yield api.parrots.list.post(null, { name, link });
    } catch (e) {
        console.log('Something went wrong!');
    }
}

...

Attaching the authenticated user

This almost works. We aren’t currently sending the user_id field to the server, but as it’s required, we get a 500 error saying that the user_id field cannot be null. It would be great if we didn’t need to send this user_id manually, but instead, Django would take the authenticated user and save the parrot to the database with that user’s ID.

Luckily for us, we can accomplish this very easily by overriding the perform_create method on the ParrotViewSet and passing the currently authenticated user to the serializer:

# parrot_mania/parrots/views.py

class ParrotViewSet(viewsets.ModelViewSet):
    ...

    def perform_create(self, serializer):
        return serializer.save(user=self.request.user)

Creating parrots should now work. However, we don’t actually see anything happening. Let’s redirect the user to /parrots once the request has finished. In the createParrot Saga, modify the createParrotSaga function like this:

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

import { push } from 'connected-react-router';
import { takeEvery, put } from 'redux-saga/effects';
...

function* createParrotSaga(createParrotAction) {
    try {
        const { name, link } = createParrotAction;
        yield api.parrots.list.post(null, { name, link });

        yield put(push('/parrots'));
    } catch (e) {
        console.log('Something went wrong!');
    }
}

...

Let’s also add a link to /parrots/create to our ParrotsList component:

# app/src/views/ParrotsList.js

...
import { Link } from 'react-router-dom';
...
const ParrotsList = ({ parrots }) => (
    <Container>
        <h1>Here are your parrots!</h1>
        <Link to="/parrots/create">
            Add a new parrot!
        </Link>
        ...
    </Container>
);

Here’s how the application should look now:

The functionality I planned for Parrot-mania is now complete but if you’re eager to continue working on this project, try adding updating and deleting parrots as well in order to make it a full CRUD app.

You can check out the full source of Parrot-mania here: https://github.com/JoosepAlviste/parrot-mania.

Conclusion

We created a simple parrots’ management web app using some interesting technologies included in Thorgate’s SPA project template. Some of those to highlight, in my opinion, are:

  • Docker makes it possible to run the application with minimal set-up
  • Server-side rendering allows us to write the UI layer of our application in React while not having to worry about search engines
  • Formik is a great package for writing forms in a super-simple way
  • Redux Saga is used to handle side effects in an organized manner

There is also a Medium post where you can leave your feedback or other comments: