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 foundsvelte-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 bynode apollo-server.js
- Start Svelte server at
localhost:5000
bynpm 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:
- Use @rollup/plugin-graphql to interpret GraphQL instead of the Apollo client's
gql
module. Interpret GraphQL (there are many implementations of this). - Load Query and Mutaion from
schema.graphql
. See below for the contents. - Import
svelte-apollo
and set the default client instance. - The return value of
query()
isStores
of Svelte wrapped with Promise, so usesubscribe()
to remove the contents. If you want to remove the contents, usesubscribe()
. You can see roughly whatStores
does by looking at here. In a word, it is an object that makes it easy to manage the state of multiple components. - Return value of Mutaion is not
Stores
. - 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,