Add reactions to your SvelteKit pages with GraphQL and Form Actions

Add reactions to your SvelteKit pages with GraphQL and Form Actions

SvelteKit is a fast, fun, and flexible framework built on Svelte and Vite. SvelteKit lets you write code that runs on the server for every page, including form actions, and data loaders.

In this guide we'll add the ability for readers to leave their feedback using emojis using a Form Action for any page.

If you don't already have a SvelteKit site, you can read the documentation to create one.

Let's begin by creating our GraphQL backend with the Grafbase CLI. Inside of your SvelteKit project run the following:

npx grafbase init

Then inside of the file grafbase/schema.graphql replace the contents with the following SDL:

type Page @model {
  url: URL! @unique
  likes: Int @default(value: 0)
  hearts: Int @default(value: 0)
  poop: Int @default(value: 0)
  party: Int @default(value: 0)
}

We'll use this model to track each individual page for our site, and increment the number of reactions using the atomic operations mutation.

Now run the GraphQL backend using the Grafbase CLI by running the following:

npx grafbase dev

You could easily use fetch to make a request to Grafbase as shown in this article but we'll opt to use a thin wrapper for the purposes of this guide.

You will want to run the following command inside of your SvelteKit project:

npm install graphql graphql-request

Using graphql-request we will import GraphQLClient to initialize a new instance we can pass to all of the pages we want to make a GraphQL request.

For the purposes of this guide we'll add the import right inside of the +page.server.ts file for your page. If you haven't already got a .server.ts, add it.

Also inside of your +page.server.ts you will want to import 2 new environment variables.

import { GRAFBASE_API_KEY, GRAFBASE_API_URL } from '$env/static/private'
import { GraphQLClient } from 'graphql-request'

const client = new GraphQLClient(GRAFBASE_API_URL, {
  headers: {
    'x-api-key': GRAFBASE_API_KEY
  }
})

Next create the file .env in the root of your project and add the following:

GRAFBASE_API_URL=http://localhost:4000/graphql
GRAFBASE_API_KEY=

When working locally we can omit the value for GRAFBASE_API_KEY. Once you deploy this to the web you will want to configure the environment variables there so they are picked up by SvelteKit and injected using $env/static/private at build time.

The first thing we will do is fetch the current reactions from our GraphQL backend. We can do this using the client we defined above and the following query:

const GetPageByUrl = /* GraphQL */ `
  query GetPageByUrl($url: URL!) {
    page(by: { url: $url }) {
      ...Reactions
    }
  }
  ${PageFragment}
`

You'll also want to define the custom fragment PageFragment in your page as we'll be reusing that in two other GraphQL operations:

const PageFragment = /* GraphQL */ `
  fragment Reactions on Page {
    likes
    hearts
    poop
    party
  }
`

Next you will want to add the load function, or add to it if it already exists. We'll fetch from the arguments passed to load the url property:

import type { PageServerLoad } from './$types';

export const load = (async ({ url }) => {
	const { page } = await client.request(GetPageByUrl, {
		url: url.href
	});

	return {
		reactions: page
	};
}) satisfies PageServerLoad;

You may have noticed above that we fetch from the backend the page where the url is of that we pass in url.href. But if the page doesn't exist, we need to create it.

We'll start by adding the mutation to create a page by url:

const CreatePageByUrl = /* GraphQL */ `
  mutation CreatePageByUrl($input: PageCreateInput!) {
    pageCreate(input: $input) {
      page {
        ...Reactions
      }
    }
  }
  ${PageFragment}
`

Then we can update the load method to invoke that mutation:

export const load = (async ({ url }) => {
	const { page } = await client.request(GetPageByUrl, {
		url: url.href
	});

	if (!page) {
		const { pageCreate } = await client.request(CreatePageByUrl, {
			input: {
				url: url.href
			}
		});

		return { reactions: pageCreate?.page };
	}

	return {
		reactions: page
	};
}) satisfies PageServerLoad;

We're now ready to present the number of reactions inside of our page.

We'll begin by creating a very simple form to our page that just uses HTML. If you have a CSS framework configured with SvelteKit, feel free to add any styles.

We'll add 4 buttons that contain value attributes that match the fields in our schema:

  • likes
  • hearts
  • poop
  • party
<form method="POST">
  <button name="reaction" type="submit" value="likes">
    👍 {data.reactions.likes}
  </button>
  <button name="reaction" type="submit" value="hearts">
    ❤️ {data.reactions.hearts}
  </button>
  <button name="reaction" type="submit" value="poop">
    💩 {data.reactions.poop}
  </button>
  <button name="reaction" type="submit" value="party">
    🎉 {data.reactions.party}
  </button>
</form>

Once the form is added you'll notice that we're using data.reactions. We'll need to export the data from our server for use inside of the template.

At the top of your +page.svelte file add the following:

<script lang="ts">
  import type {PageData} from './$types'; export let data: PageData;
</script>

You should now have something that looks like this (with your own styling of course):

Form with reactions

Now that reactions are shown on our page we will now write the action to handle form submissions.

Since we're using the value attribute of the <button> element we can fetch from the formData of the request with SvelteKit and pass that onto our backend.

import type {  Actions } from './$types';

type Reaction = 'likes' | 'hearts' | 'poop' | 'party';

export const actions = {
	default: async ({ request, url }) => {
		const data = await request.formData();
		const reaction = data.get('reaction');

		const { page } = await client.request(GetPageByUrl, {
			url: url.href
		});

		await client.request(IncrementByUrl, {
			url: url.href,
			input: {
				[reaction as Reaction]: {
					increment: 1
				}
			}
		});

		return { success: true };
	}
} satisfies Actions;

You should now see when you click the buttons for each emoji that the count is updated via the form action, and the new values are shown!

Finished reaction widget preview

Build a SvelteKit backend with GraphQL

Deploy your Svelte backend in minutes with Grafbase.