GraphQL becomes the one and only mediator between your frontend and your backend.
Tom Preston-Werner - RedwoodJS with Tom Preston-Werner
Introduction | Tutorial |
---|---|
I - Redwood Philosophies | Part 1 - Setup |
II - Fullstack React | Part 2 - Routes |
III - Jamstack | Part 3 - Prisma |
IV - Serverless | Part 4 - Cells |
Part 5 - Contact | |
Part 6 - GraphQL | |
Part 7 - Deploy | |
Part 8 - Auth |
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())
}
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 redwood db save
to create a migration schema.
yarn rw db save
It will automatically assign your migration a name.
This will create another migrations
directory with a schema file, README, and steps.json
file like we saw in part 3.
Enter yarn redwood db up
to apply that migration to the database.
yarn rw db up
This will output the database actions applied after the migration, which includes one CreateTable
statement.
Lastly the Prisma Client is generated.
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
This will create an sdl in your graphql
folder and a resolver in your services
folder.
We created three files:
-
contacts.sdl.js
in thegraphql
folder -
contacts.js
in theservices/contacts
folder -
contacts.test.js
in theservices/contacts
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
}
`
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.
type Mutation {
createContact(input: CreateContactInput!): Contact
}
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()
}
We'll create a resolver for createContact
.
// api/src/services/contacts/contacts.js
export const createContact = ({ input }) => {
return db.contact.create({ data: input })
}
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.
const CREATE_CONTACT = gql`
mutation CreateContactMutation($input: CreateContactInput!) {
createContact(input: $input) {
id
}
}
`
We will just return the id
since the user will not see this.
Make sure to import useMutation
at the top of the file.
import { useMutation } from '@redwoodjs/web'
Inside the ContactPage
component we'll call useMutation()
. We pass an object create
with variables that are defined in CREATE_CONTACT
.
const ContactPage = () => {
const [create] = useMutation(CREATE_CONTACT)
const onSubmit = (data) => {
create({variables: {input: data}})
console.log(data)
}
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.
Our form gives us the following object which we saw in the previous part.
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
?
This is how we access the graphiQL interface. Click the graphql link.
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.
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 your entered into your form.
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.
- We want to give the user some immediate feedback after they click the save button.
- 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 a loading
object after create
.
const [create, { loading } ] = useMutation(CREATE_CONTACT)
loading
will be true while the mutation is being performed, and will change to false when the mutation is complete.
We will add disabled
to the Submit
button and pass it the loading
object.
<Submit disabled={loading}>Save</Submit>
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.
Enter some more data and click the Save button.
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
.
import { useForm } from 'react-hook-form'
Inside our ContactPage
component we'll create a formMethods
variable and assign it the useForm()
function that we just imported.
const formMethods = useForm()
Inside <Form>
we'll add formMethods
and pass it formMethods
as a prop.
<Form
onSubmit={onSubmit}
validation={{ mode: 'onBlur' }}
formMethods={formMethods}
>
This gives us access to the reset function. In useMutation
we'll add a second parameter, which is a callback called onCompleted
.
const ContactPage = () => {
const formMethods = useForm()
const [create, { loading } ] = useMutation(CREATE_CONTACT, {
onCompleted: () => {
formMethods.reset()
alert('Thanks for telling me stuff about my things!')
}
})
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.
First the alert message appears on the screen. After clicking OK the form will be 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>
Check your form for the changes.
In the next part we'll finally deploy our project to the internet with Netlify and Heroku.
Discussion