Connecting Svelte to Apollo

·

6 min read

Well, the weather has been so nice that I've been neglecting it on the weekends while BBQing and making pizzas, but I think it's time to start working on the client side. Now that we've got the server side working, let's make a client that connects to it.

Apollo's official website says that they don't know any client other than React, which is sad, but I'll try to make a TODO app client with Svelte.

TL;DR

  • I tried to run a GraphQL client with Svelte and Apollo.
  • You can do it with @apollo/client, but I found svelte-apollo, which is also introduced by Apollo, to be quite good. svelte-apollo is not well documented comparing to other ones, so I'll write what I wish it had.
  • Svelte recommends rollup as a task runner, and I'll write about what to watch out for in rollup.

I think some parts of the readme are slightly wrong, maybe because the version of Apollo has been upgraded, but some parts don't work as is.

Let's prepare for Svelte and Apollo Client

Here is my project structure

svelte-apollo-todo/
├── apollo 
└── svelte

Installing Svelte...

cd svelte-apollo-todo/
npx degit sveltejs/template svelte
cd svelte
npm install

and GraphQL...

npm i --save @apollo/client graphql

Starting Apollo and Svelte

  • Start the Apollo server at localhost:4000 with the code we created last time by node apollo-server.js
  • Start Svelte server at localhost:5000 by npm run dev

Now, if you change App.svelte to the following, it will work for now. Here, gql interprets the syntax of GraphQL and turns it into a JS module.

import { InMemoryCache, ApolloClient, gql } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache()
});

client
  .query({
    query: gql`
      query list {
        tasks {
          id
          name
          isActive
          createdAt
          updatedAt
          owner
        }
      }
    `
  })
  .then(result => console.log(result));

The respons is like:

{data: {…}, loading: false, networkStatus: 7}
  data:
    tasks: Array(4)
        0: {__typename: "Task", id: "1", name: "Soak in an Onsen", isActive: true, createdAt: null, …}
        1: {__typename: "Task", id: "2", name: "Sing Karaoke", isActive: false, createdAt: null, …}
        2: {__typename: "Task", id: "3", name: "See cherry blossom", isActive: true, createdAt: null, …}
        3: {__typename: "Task", id: "4", name: "Buy some milk", isActive: true, createdAt: 1621812683, …}
    length: 4
      __proto__: Array(0)
    __proto__: Object
  loading: false
  networkStatus: 7
  __proto__: Object

At the top level of the response, there is loading: false, so I guess I can control whether it is loading or finished. Well, this way is fine, but I found svelte-apollo which seems to make integration with Svelte easier, so I'll try that way.

Using svelte-apollo

svelte-apollo is Svelte integration for Apollo GraphQL.

Install

cd svelte
npm i --save svelte-apollo @rollup/plugin-graphql @rollup/plugin-replace

dependencies in package.json at this point is like this.

  "dependencies": {
    "@apollo/client": "^3.3.19",
    "@rollup/plugin-graphql": "^1.0.0", // Use this loader
    "graphql": "^15.5.0", // Don't use this anymore
    "sirv-cli": "^1.0.0",
    "svelte-apollo": "^0.4.0"
  }

Here's an App.svelte that does a whole bunch of operations on the GraphQL backend of TODO

import { InMemoryCache, ApolloClient } from '@apollo/client'; // ❶
import { setClient, query, mutation } from "svelte-apollo"; // ❸
import { list_tasks, get_task, add_task, complete_task, delete_task } from './schema.graphql'; // ❷

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache()
});
setClient(client); // ❸

// list
const listTasks = async () => {
  const reply = await query(list_tasks);
  reply.subscribe(data => console.log('list', data)); // ❹
};
listTasks();

// getTask
const getTask = async (tid) => {
  const reply = await query(get_task, { variables: { id: tid } });
  reply.subscribe(data => console.log('get', data));
};
getTask(3);

// addTask
const add = mutation(add_task);
const addTask = async (tname) => {
  const reply = await add({ variables: { name: tname } }); // ❺
  console.log('add', reply) 
};
addTask('New task');

// deleteTask
const del = mutation(delete_task);
const deleteTask = async (tid) => {
  const reply = await del({ variables: { id: tid } });
  console.log('del', reply) 
};
deleteTask(5);

// completeTask
const done = mutation(complete_task);
const completeTask = async (tid) => {
  const reply = await done({ variables: { id: tid } });
  console.log('done', reply) // ❻
};
completeTask(10);

Changes from using the Apollo client as-is:

  1. Use @rollup/plugin-graphql to interpret GraphQL instead of the Apollo client's gql module. Interpret GraphQL (there are many implementations of this).
  2. Load Query and Mutaion from schema.graphql. See below for the contents.
  3. Import svelte-apollo and set the default client instance.
  4. The return value of query() is Stores of Svelte wrapped with Promise, so use subscribe() to remove the contents. If you want to remove the contents, use subscribe(). You can see roughly what Stores does by looking at here. In a word, it is an object that makes it easy to manage the state of multiple components.
  5. Return value of Mutaion is not Stores.
  6. Not the main topic, but it is easy to understand if you label it as console.log('label', reply).

schema.graphql is as below.

query list_tasks {
  tasks {
    id
    name
    isActive
    createdAt
    updatedAt
    owner
  }
}

query get_task($id: ID!) {
  task(id: $id) {
    id
    name
  }
}

mutation add_task($name: String!) {
  addTask(name: $name) {
    id
    name
  }
}

mutation complete_task($id: ID!) {
  completeTask(id: $id) {
    id
    name
    isActive
  }
}

mutation delete_task($id: ID!) {
  deleteTask(id: $id) {
    id
    name
  }
}

Corresponging reply

list {loading: false, data: {…}, error: undefined}
  data:
    tasks: Array(4)
      0: {__typename: "Task", id: "1", name: "Soak in an Onsen", isActive: true, createdAt: null, …}
      1: {__typename: "Task", id: "2", name: "Sing Karaoke", isActive: false, createdAt: null, …}
      2: {__typename: "Task", id: "3", name: "See cherry blossom", isActive: true, createdAt: null, …}
      3: {__typename: "Task", id: "4", name: "Buy some milk", isActive: true, createdAt: 1621812683, …}
      length: 4
      __proto__: Array(0)
    __proto__: Object
  error: undefined
  loading: false
  __proto__: Object

The above is a console.log() to look into the contents of the object, but in reality, the Svelte way is to handle it as follows. It's nice and clean.

const reply = query(list_tasks);

{#if $reply.loading}
  Loading...
{:else if $reply.error}
  Error: {$reply.error.message}
{:else}
  {#each $reply.data.tasks as task}
    <p>{task.id} {task.name}</p>
  {/each}
{/if}

You can now do CRUD with GraphQL. Now we just need to tweak the HTML side of App.svelte.

Rollup error collection

Since I am a beginner, Rollup-sensei gave me a few scolding.

Error 1

bundle.js:5019 Uncaught ReferenceError: process is not defined
    at new ApolloClient (bundle.js:5019)

This is not passing environment variables properly. The following @rollup/plugin-replace solves it.

rollup.config.js

    replace({
      'process.env.NODE_ENV': JSON.stringify( 'development' )
    }),

Error 2

Error: Unexpected token (Note that you need plugins to import files that are not JavaScript)

GraphQL is not being interpreted. Solved with @rollup/plugin-graphql

Error 3

This is not a rollup, but an Apollo error. cache is required.

Uncaught Invariant Violation: To initialize Apollo Client, you must specify a 'cache' property in the options object.

Error 4

What kind of error was this? I don't think it had anything to do with the execution.

Exception: TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for ca

Cheers,