a first look at redwoodJS part 6 - sdl, useMutation

ajcwebdev - Jul 26 '20 - - Dev Community

GraphQL becomes the one and only mediator between your frontend and your backend.

Tom Preston-Werner - RedwoodJS with Tom Preston-Werner

Part 6 - SDL, useMutation

In part 5 we generated a contact page and took input from the user. In this part we'll connect our contact form to our database so we can persist the data entered into the form.

6.1 Contact

To modify our database we'll go to our schema.prisma file and add a Contact model.

// api/db/schema.prisma

model Contact {
  id        Int      @id @default(autoincrement())
  name      String
  email     String
  message   String
  createdAt DateTime @default(now())
}
Enter fullscreen mode Exit fullscreen mode

The model takes the name, email, and message strings from the form inputs. It creates an id with autoincrement() and createdAt time with now().

Enter yarn rw prisma migrate dev to create a migration.

yarn rw prisma migrate dev --name contact-it
Enter fullscreen mode Exit fullscreen mode
Running Prisma CLI:

Prisma schema loaded from db/schema.prisma
Datasource "DS": PostgreSQL database "railway", schema "public" at "containers-us-west-1.railway.app:5884"

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20210307135440_contact_it/
    └─ migration.sql

✔ Generated Prisma Client (2.16.1) to ./../node_modules/@prisma/client in 66ms

Everything is now in sync.
Enter fullscreen mode Exit fullscreen mode
CREATE TABLE "Contact" (
    "id" SERIAL NOT NULL,
    "name" TEXT NOT NULL,
    "email" TEXT NOT NULL,
    "message" TEXT NOT NULL,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY ("id")
)
Enter fullscreen mode Exit fullscreen mode

6.2 redwood generate sdl

Enter yarn redwood generate sdl contact to create the schema definition language for the contact form.

yarn rw g sdl contact
Enter fullscreen mode Exit fullscreen mode
✔ Generating SDL files...
  ✔ Successfully wrote file `./api/src/graphql/contacts.sdl.js`
  ✔ Successfully wrote file `./api/src/services/contacts/contacts.scenarios.js`
  ✔ Successfully wrote file `./api/src/services/contacts/contacts.test.js`
  ✔ Successfully wrote file `./api/src/services/contacts/contacts.js`
Enter fullscreen mode Exit fullscreen mode

This will create an sdl in your graphql folder and a resolver in your services folder.

06-api-src-folder

6.3 contacts.sdl.js

We'll look at our schema definition language first.

// api/src/graphql/contacts.sdl.js

export const schema = gql`
  type Contact {
    id: Int!
    name: String!
    email: String!
    message: String!
    createdAt: DateTime!
  }

  type Query {
    contacts: [Contact!]!
  }

  input CreateContactInput {
    name: String!
    email: String!
    message: String!
  }

  input UpdateContactInput {
    name: String
    email: String
    message: String
  }
`
Enter fullscreen mode Exit fullscreen mode

The sdl has two types:

  • Contact
  • Query

And two inputs:

  • CreateContactInput
  • UpdateContactInput

6.4 createContact

We need to create our own mutation. We don't get this by default because the sdl generator doesn't know what we plan to do with the schema.

// api/src/graphql/contacts.sdl.js

export const schema = gql`
  type Contact {
    id: Int!
    name: String!
    email: String!
    message: String!
    createdAt: DateTime!
  }

  type Query {
    contacts: [Contact!]!
  }

  input CreateContactInput {
    name: String!
    email: String!
    message: String!
  }

  input UpdateContactInput {
    name: String
    email: String
    message: String
  }

  type Mutation {
    createContact(
      input: CreateContactInput!
    ): Contact
  }
`
Enter fullscreen mode Exit fullscreen mode

Our Mutation type will be called createContact. The input is CreateContactInput! and it returns Contact.

6.5 contacts.js

Now we'll look at our contacts service. It has a resolver for the Query type from our sdl.

// api/src/services/contacts/contacts.js

import { db } from 'src/lib/db'

export const contacts = () => {
  return db.contact.findMany()
}
Enter fullscreen mode Exit fullscreen mode

We'll create a resolver for createContact.

// api/src/services/contacts/contacts.js

import { db } from 'src/lib/db'

export const contacts = () => {
  return db.contact.findMany()
}

export const createContact = ({ input }) => {
  return db.contact.create(
    { data: input }
  )
}
Enter fullscreen mode Exit fullscreen mode

This takes input from the browser and adds it to the database.

6.6 CREATE_CONTACT

In our ContactPage file we'll add the mutation and assign it to CREATE_CONTACT. It is a common convention to use all caps for our mutations.

// web/src/pages/ContactPage/ContactPage.js

const CREATE_CONTACT = gql`
  mutation CreateContactMutation($input: CreateContactInput!) {
    createContact(input: $input) {
      id
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

We will just return the id since the user will not see this. Make sure to import useMutation at the top of the file.

// web/src/pages/ContactPage/ContactPage.js

import { useMutation } from '@redwoodjs/web'
Enter fullscreen mode Exit fullscreen mode

Inside the ContactPage component we'll call useMutation(). We pass an object create with variables that are defined in CREATE_CONTACT.

// web/src/pages/ContactPage/ContactPage.js

const ContactPage = () => {
  const [create] = useMutation(CREATE_CONTACT)

  const onSubmit = (data) => {
    create({
      variables: {
        input: data
      }
    })
    console.log(data)
  }

  return (...)
}
Enter fullscreen mode Exit fullscreen mode

The input is the actual object containing the data which is assigned to variables and passed to the create() function inside onSubmit. We are now ready to enter our data. Enter a name, email, and message and click the Save button.

07-form-input

Our form gives us the following object which we saw in the previous part.

08-form-input-console-log

6.7 graphiQL

We can now check to make sure it was saved. Remember all the way back in part 1 when we said there was something else running on localhost:8911?

09-localhost-8911

This is how we access the graphiQL interface. Click the graphql link.

10-graphiQL

GraphiQL is like an IDE for your GraphQL queries. On the left side you'll enter a query, and after clicking the middle play button the response will appear on the right side.

We'll create a contact query that will return all the contacts in the database.

query ContactQuery {
  contacts {
    id
    name
    email
    message
  }
}
Enter fullscreen mode Exit fullscreen mode

We'll ask for the id, name, email, and message.

Click the play button and on the right side you'll receive a data object containing whatever you entered into your form.

12-contact-data

The data object contains a contacts array with objects containing the form input. There's a few more things we can add to our form to make the user experience a little better.

  1. We want to give the user some immediate feedback after they click the save button.
  2. We also want to put in safeguards in case the user feels like clicking the save button many times in fast succession.

6.8 loading

To do this we'll add two objects after create:

  • loading
  • error
// web/src/pages/ContactPage/ContactPage.js

const ContactPage = () => {
  const [
    create,
    { loading, error }
  ] = useMutation(CREATE_CONTACT)

  const onSubmit = (data) => {...}
  return (...)
}
Enter fullscreen mode Exit fullscreen mode

loading will be true while the mutation is being performed, and will change to false when the mutation is complete. Add disabled to the Submit button and pass it the loading object.

// web/src/pages/ContactPage/ContactPage.js

return (
  // ...
  <Submit disabled={loading}>
    Save
  </Submit>
  // ...
)
Enter fullscreen mode Exit fullscreen mode

While the mutation is in progress disabled is true, and once it is complete it will change to false. To test this out go to your Network tab in your browsers devtools. Here we can simulate slow 3G.

13-slow-3G

Enter some more data and click the Save button.

14-save-button-greyed-out

Now when you click the Save button it will be greyed out for a moment while the input is saved to the database.

6.9 useForm

Another problem with this form is the input sticks around on the page even after it is submitted. It would be better if we cleared the fields after submitting. We can do this by importing useForm from react-hook-form.

// web/src/pages/ContactPage/ContactPage.js

import { useForm } from 'react-hook-form'
Enter fullscreen mode Exit fullscreen mode

Inside our ContactPage component we'll create a formMethods variable and assign it the useForm() function that we just imported.

// web/src/pages/ContactPage/ContactPage.js

const ContactPage = () => {
  const formMethods = useForm()
  //...
Enter fullscreen mode Exit fullscreen mode

Inside <Form> we'll add formMethods and pass it formMethods as a prop.

// web/src/pages/ContactPage/ContactPage.js

<Form
  onSubmit={
    onSubmit
  }
  validation={{
    mode: 'onBlur'
  }}
  formMethods={formMethods}
>
Enter fullscreen mode Exit fullscreen mode

This gives us access to the reset function. In useMutation we'll add a second parameter, which is a callback called onCompleted.

// web/src/pages/ContactPage/ContactPage.js

const ContactPage = () => {
  const formMethods = useForm()
  const [create, { loading } ] = useMutation(CREATE_CONTACT, {
    onCompleted: () => {
      formMethods.reset()
      alert('Thanks for telling me stuff about my things!')
    }
  })
Enter fullscreen mode Exit fullscreen mode

When the mutation is complete we'll call formMethods.reset() and display an alert() saying Thanks for telling me stuff about my things! To test this in the browser, enter some more data and click the Save button.

15-alert-thanks-for-the-message

First the alert message appears on the screen. After clicking OK the form will be cleared.

16-form-cleared

Another thing we can improve is the label. Right now it is just defaulting to whatever is entered as the name attribute in <Label>. We can add a closing </Label> tag and enter something between the tags to change the label. We'll capitalize the labels.

<Label
  name="name"
  errorClassName="error"
>
  Name
</Label>

<Label
  name="email"
  errorClassName="error"
>
  Email
</Label>

<Label
  name="message"
  errorClassName="error"
>
  Message
</Label>
Enter fullscreen mode Exit fullscreen mode

Check your form for the changes.

17-updated-form-labels

In the next part we'll add authentication to our project so the haxz0rs can't steal your Dogecoin.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .