August 1st, 2021 - 13 min read
We're adding the frontend code now. So we'll create a basic to-do app with a database to store the items.
I'm using Typescript but with smaller changes, the code will also work as Javascript. Some Svelte knowledge is required as I'm not explaining the basics.
If you need a Svelte introduction, I can highly recommend doing the complete Svelte basics tutorial. It's perfect to get up and running quickly. As you can practice right after learning the new things.
Anyway, I'm adding links to the basics tutorial so you can learn about the points you're missing.
This is part two. If you like, check out the first post about the Docker setup here - recommended so you have a working backend service.
First, install the required dependencies in the folder app
(your Svelte app folder). We need to run npm install @apollo/client subscriptions-transport-ws ws apollo-utilities graphql graphql-tag -SE
Some words to the dependencies, @apollo/client
is used to get the data with Graphql from our backend service that we've already created.
subscriptions-transport-ws
is used for our Websocket link to have our todos update in real-time. As mentioned in the readme, the dependency is not actively maintained anymore but it's used inside @apollo/client
but I think it's OK to use for now.
apollo-utilities
is used later to split the Apollo link in HTTP and Websocket.
graphql
and qraphql-tag
are required for the queries with Apollo.
ws
for the WebSocket implementation in Node.js.
Note
We're not using
svelte-apollo
because of an issue with bundling or Svelte context. Please have a look at the section Svelte-apollo below.
Run docker-compose up
or right-click on the docker-compose file and select Compose up
to start all services of our project.
Next, run npm run dev
as we're developing on localhost:3000
and using Postgres on localhost:8080
. With this, you will have hot reloading on any changes.
At localhost:80
you have the build result of your Svelte app (no hot reloading) running in a container as a Node.js app.
We're creating most of the code inside of src\routes\index.svelte
. This is OK to show how to work with Graphql. But in a larger app, you'd use multiple components and have the queries/mutations in a separate file.
Add the file app/src/lib/graphql-client.ts
with the following content:
import { HttpLink, ApolloClient, split } from '@apollo/client/core/core.cjs.js';
import { InMemoryCache } from '@apollo/client/cache/cache.cjs.js';
import { WebSocketLink } from '@apollo/client/link/ws/ws.cjs.js';
import fetch from 'cross-fetch';
import { getMainDefinition } from 'apollo-utilities'
import websocket from 'ws';
import { browser } from '$app/env';
// Create an http link
// (needed for adding items)
const httpLink = new HttpLink({
uri: 'http://localhost:8080/v1/graphql',
fetch
})
// Create a WebSocket Link
// (needed for realtime updates)
const webSocketConfig:WebSocketLink.Configuration = {
uri: 'ws://localhost:8080/v1/graphql',
options: {
reconnect: true
}
}
if (!browser) {
webSocketConfig.webSocketImpl = websocket
}
const wsLink = new WebSocketLink(webSocketConfig)
// const wsLink = WebSocketLink
// Using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
httpLink
)
export const client:ApolloClient = browser ? new ApolloClient({
link,
cache: new InMemoryCache(),
ssrForceFetchDelay: 100,
})
: new ApolloClient({
// Make sure queries run once
ssrMode: true,
// Use SchemaLink to run queries on local scheme (no round trips)
// new SchemaLink(...) // how does it work?
link,
// Cache not uses, but required by Apollo
cache: new InMemoryCache(),
// Disbale all cache
defaultOptions: {
query: {
fetchPolicy: 'no-cache'
},
mutate: {
fetchPolicy: 'no-cache'
},
watchQuery: {
fetchPolicy: 'no-cache'
}
}
})
We're creating two Graphql links, one for the HTTP and one for the WebSocket connection. HTTP is used for queries and mutations and the Websocket will be used to subscribe for changes.
cross-fetch
is needed for the production build, so fetch is available in Node.js.
We're not using a Svelte context as it's easier to directly import the client
where we need it. Svelte-apollo is using the Context approach and that's not working.
The context was working if you're setting the context in __layout.svelte
and use it in index.svelte
with getContext
but it wasn't with Svelte-apollo
.
What is split
doing?
It splits the query, if the return value of the callback evaluates to true
it will use the WebSocket link. Otherwise, it's using the HTTP Link.
Finally, it's exporting the client with a condition. For the browser, it will return with InMemoryCache
and caching enabled and for the server-side render, it will disable caching completely.
Why .cjs.js at the imports? This is needed so Sveltekit is correctly bundling the files. Not sure if there is a better option but this is working. I think this will change in the future but for now, I'm OK with doing it like this. If you're having more details, please add a comment below.
Side note Imports would be easier if there would be an ESM version available as recommended in the Sveltekit FAQ. Maybe a Vite plugin could help here to convert the CommonJS to ESM before Sveltekit starts. Not sure if that's possible but I have to search for it.
Svelte configuration in svelte.config.js
Add tslib
to the Vite sss.noExternal
so the production build is working.
svelte.config.js
import sveltePreprocess from 'svelte-preprocess'
import node from '@sveltejs/adapter-node'
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: sveltePreprocess(),
kit: {
// By default, `npm run build` will create a standard Node app.
// You can create optimized builds for different platforms by
// specifying a different adapter
adapter: node(),
// hydrate the <div id="svelte"> element in src/app.html
target: '#svelte',
vite: {
ssr: {
noExternal: [ 'tslib' ]
}
}
},
};
export default config;
The @sveltejs/adapter-node
is important for the build. This will create the final node.js app in the build
folder that will serve our Svelte app with Docker on port 80.
With a subscription, we get notified about changes to our todos.
We could also query the database but if you'd like to have the updates in real-time, a subscription is the way to go. A to-do list is probably not the best use-case but for something like chats or comments, it's perfect.
In app/src/routes/index.svelte
add this code:
<script lang="ts">
import { gql } from '@apollo/client/core/core.cjs.js';
import { client } from '$lib/graphql-client';
const initialTodo = {
title: '',
completed: false
};
let newTodo = {
...initialTodo
};
type Todo = typeof newTodo & {
id;
};
const queryTodo = gql`
subscription TodosQuery {
todo {
completed
id
title
}
}
`;
// todos will be a store --> in html we need to access this with $todos (Svelte store)
type TodoStoreType = {
data: {
todo: Todo[] | [];
};
};
let todos: SvelteStore<TodoStoreType> = client.subscribe({query: queryTodo});
</script>
The newTodo
object we will be used to create a new todo later.
Mutation query for this we'll add after the subscription part is working.
We're importing our Apollo client and create a Graphql query. It uses gql
to generate a query object.
If you haven't seen the syntax before, it's a tagged template function and it's like calling function gql('mutli-line query string here')
.
It's a subscription to the todo table and we're interested in completed
, id
, and title
keys.
As the last step, we're using the Apollo client to subscribe with the generated subscription query. This will return a Svelte store that can be accessed in the markup with $todos
.
As next part of the index.svelte
, we have to add our component html to render the data we received from the subscription:
<main>
<h1>Todos</h1>
{#if !$todos}
Loading todos...
{:else if $todos.data}
<ul>
{#each $todos.data.todo as todo (todo.id)}
<li>
{todo.title} - {todo.completed ? "done" : "open"}
</li>
{/each}
</ul>
{/if}
</main>
If $todos
is undefined we're showing a loading text, once the data key is available we're iterating through the todos array with an each block {#each $todos.data.todo as todo}
and render the title and the status of the todo.
The last part in the each
block is used by Svelte to track each item by its unique id instead of adding/removing items at the end of the list.
Insert in the index.svelte
below the h1
this form code:
<form on:submit|preventDefault={insertTodo}>
<input placeholder="enter Todo title" bind:value={newTodo.title}/>
<label for="completed">Completed: <input id="completed" type="checkbox" bind:checked={newTodo.completed}/></label>
<button type="submit">Submit</button>
</form>
This will bind the text input to newTodo.title
and the checkbox to newTodo.completed
.
The on:submit
is using preventDefault
to prevent the default browser behavior that would trigger a page reload. Once the form is submitted the insertTodo
function is called.
Add the insertTodo
code to the script tag:
const insertMutation = gql`
mutation InsertTodo($title: String!, $completed: Boolean) {
insert_todo(objects: {completed: $completed, title: $title}) {
affected_rows
returning {
id
}
}
}
`
function insertTodo() {
client.mutate({
mutation: insertMutation,
variables: newTodo
});
newTodo = {
...initialTodo
};
}
The insertMutation
is the mutation query with a required $title
parameter and an optional $completed
. The title is required because there is an exclamation mark behind String
.
The body of the mutation is the return value that we're getting once the mutation was successful. It returns the number of affected rows and the id of the generated todo.
Now, we can use the Apollo client to do the mutation inside the insertTodo
and pass the mutation query and the variables.
The variables will be used in the query and the passed object must match the query e.g. {title: "Do something", completed: false}
Create a delete mutation in index.svelte
:
const deleteMutation = gql`
mutation DeleteTodo($id: uuid!) {
delete_todo_by_pk(id: $id) {
id
title
}
}
`;
We're using the primary key, the id, to delete the todo.
Add a deleteTodo
method:
const deleteTodo = id => {
client.mutate({
mutation: deleteMutation,
variables: { id }
});
};
Finally, add a button to each todo inside the each
loop and pass the id to the delete function:
<button on:click={() => deleteTodo(todo.id)}>x</button>
In index.svelte
, add a mutation to set the todo completed value:
const setTodoCompletedMutation = gql`
mutation SetTodoCompleted($id: uuid!, $completed: Boolean)
{
update_todo_by_pk(pk_columns:
{ id: $id },
_set: { completed: $completed }) {
id
}
}
`;
Again, we're using the id to update the to-do status.
Create the toggleMethod
:
const toggleTodoCompleted = (todo) => {
const { id, completed } = todo;
client.mutate({
mutation: setTodoCompletedMutation,
variables: {
id,
completed: !completed
}
});
};
And place the on:click
on the todo:
{#each ... as todo}
<li on:click={() => toggleTodoCompleted(todo)}>
{/* todo item */}
</li>
{/each
Note: Without the key in the
#each
loop every toggle will change the order of the list. To avoid this add thetodo.id
as key to theeach loop
{#each $todos.data.todo as todo (todo.id)}
If you're changing the Svelte config or adding includes that are breaking the dev server, always check that npm run dev
and npm run build
are working. After a successful build, you can run npm run preview
to test the build locally.
It's important because you can fix the dev server but the build can have a problem.
Some errors messages & possible fixes
During the work on the demo I've had many errors and here are some points that should help to fix them.
For me, some of the issues are still unclear. Maybe I have to learn more about the internals of Sveltekit and how Vite is working. But from the Github issues, I can see that I'm not alone with the problems.
exports not defined
you have to check your configuration e.g. noExternal
can help or check if your imports are correct.Error: Function called outside component initialization
this is caused by setContext/getContext from Svelte - happend to me with svelte-apollo
where I couldn't find a solution. Important setContext
must be in a parent and getContext
at a child component.I wanted to use the library Svelte-apollo as a thin wrapper around Apollo/client.
But I couldn't get it to work and I tried many different configurations and nothing worked.
The main issue is probably the setContext
from svelte-apollo
but the proposed fix from vite-plugin-svelte
is not working.
I tried the following configurations:
optimzeDeps:exclude
for svelte-apollo
as mentioned here - not workingresolve: alias
for svelte-apollo
as proposed in issue/97 - not workingoptimize: include
from the next comment in issue 97 - doesn't helpFor now, I'm going without Svelte-apollo
and I think it's OK to directly use @apollo/client
.
If there is a fix, please let me know in the comments or the Github repo link in Conclusion below.
So you should get an idea of how to work with Graphql in Svelte.
Sure, for a real-world app you'd structure your app and queries better. Maybe I'll create another post on how to improve this point.
Things I'd like to cover in separate posts:
Besides that, I'd like to check if I can get Svelte-Apollo to work with Sveltekit without the troubles. If you could get it to work, please add a comment below or write me on Twitter.
Svelte-Apollo
should be like, run npm install
and use it in __layout.svelte
to use setContext
so in every route we can directly use mutation
, query
or subscribe
helpers that are using the context to get the Apollo client instance.
For the Svelte related part, the following points require some attention:
If you've found this post useful, please share it on social media. If there is anything to improve please let me know.
You can find the source code of this post on github.com/awolf81/my_docker_app.