Blog A.Wolf

Blog Posts

No results for 'undefined'Powered by Algolia

Svelte Docker app accessing Database with Graphql - part 2 of 2 - Svelte

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.

Dependency installation (Svelte app)

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.

Start development

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.

Create a Graphql client service

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.

Add a subscription to load the todos

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.

Add new todos

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}

Delete todos

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>

Toggle the status

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 the todo.id as key to the each loop {#each $todos.data.todo as todo (todo.id)}

Troubleshooting Svelte

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.

Svelte-apollo

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 working
  • resolve: alias for svelte-apollo as proposed in issue/97 - not working
  • optimize: include from the next comment in issue 97 - doesn't help

For 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.

Conclusion

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:

  • Authentification with Hasura - Creating sign-up & login (incl. oAuth)
  • Use Tailwind CSS to style your Svelte app (the final code is using Tailwind but nothing is mentioned here)
  • Improved structure of a Svelte app with Graphql queries (as mentioned before)
  • Deploy the app to Heroku with docker-compose context

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:

  • How to get server-side rendering to work? I think rehydration is not working in the demo.
  • Add some Svelte animations to the app. E.g. animate adding new items or initial render animation.

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.

©2024 Alexander Wolf