Build a realtime websocket UI using SvelteKit
Tags: javascript, svelte, webdev, typescript
Published: 2022-02-22 20:11:39

We used SvelteKit to make https://typedwebhook.tools - which allows you to test HTTP requests and automatically generates typescript types for the body.  Inspired by that, let's run through how to make a realtime websocket UI using SvelteKit.

If you want a sneak preview, the code for typed webhooks is open source: [https://github.com/inngest/typedwebhook.tools](https://github.com/inngest/typedwebhook.tools)

## Why SvelteKit?

- Using Vite, it builds fast in dev. And it has HMR with state persistence out of the box. Somehow this is consistently broken in every react app, no matter what kit you use :D
- It has SSR out of the box. It’s built for progressive enhancement, and configuring pre-rendering is the easiest I’ve seen
- State management is easy. It’s easy to work with stores. You can (broadly speaking) use the stores from anywhere: no top-level context necessary (ahem, hello websockets!)
- SvelteKit comes with a standard way of doing things (CSS, JS, forms, state, routing), so it’s easy to work with and it’s easy to share amongst devs. It’s easy to get set up and running with your entire framework — think a mixture of NextJS and create-react-app for Svelte.

Plus, **it's amazingly developer friendly**

## Getting started

Make sure that you have Node & NPM installed, then run:

```npm init svelte@next```

It'll run you through a guide to set up your base project.  Here's how we answered those questions:

```
✔ Where should we create your project?
  (leave blank to use current directory) … realtime-demo
✔ Which Svelte app template? › Skeleton project
✔ Use TypeScript? … Yes
✔ Add ESLint for code linting? … Yes
✔ Add Prettier for code formatting? … Yes
```

Let's go to that directory and run the dev server:

```cd ./realtime-demo && yarn dev```

If you go to localhost:3000 you should see Svelte up and running!

The current code lives in `./src`.  The routes folder (`./src/routes`) acts as a router:  `./src/routes/index.svelte` is the index page rendered by default, and `./src/routes/about.svelte` is rendered when you navigate to /about.

You might be asking yourself "where do shared components go?".  They go in `./src/lib` which isn't made by default.

Let's jump to real-time state - the meat of what we're building.

## Real-time state

[State is saved in `stores`](https://svelte.dev/tutorial/writable-stores).  A store is similar to react's `useState` value, but way more powerful.  We're going to be creating a store that records websocket responses. 

Let's make a file for our store in the shared directory: `./src/lib/state.ts`.  

Inside it, we're going to use Svelte's [writeable stores](https://svelte.dev/tutorial/writable-stores):

```typescript
// Import the function which initializes a new mutable store.
import { writable } from 'svelte/store';

type Item = {
  id: string;
  content: string;
};

// Our store will record an object containing an array of
// items produced by the websocket.
type State = {
  items: Array<Item>;
  error?: string;
};

// That's it;  state is now usable!  Components can subscribe
// to state changes, and we can mutate the store easily.
//
// Note that this is a singleton.
export const state = writable<State>({
  items: []
});

// We also want to connect to websockets.  Svelte does
// server-side rendering _really well_ out of the box, so
// we will export a function that can be called by our root
// component after mounting to connnect
export const connect = (socketURL: string) => {
  const ws = new WebSocket(`wss://${socketURL}`);
  if (!ws) {
    // Store an error in our state.  The function will be
    // called with the current state;  this only adds the
    // error.
    state.update((s: State) => return {...s, error: "Unable to connect" });
    return;
  }

  ws.addEventListener('open', () => {
    // TODO: Set up ping/pong, etc.
  });

  ws.addEventListener('message', (message: any) => {
    const data: Item = JSON.parse(message.data);
    // Mutate state by prepending the new data to the array.
    state.update((state) => ({ ...state, items: [data].concat(state.items) }));
  });

  ws.addEventListener('close', (_message: any) => {
    // TODO: Handle close
  });
}
```

We can now use this in our index page, `./src/routes/index.svelte`:

```typescript
<script context="module" lang="ts">
  export const prerender = true;
</script>

<script lang="ts">
  import { onMount } from 'svelte';
  // $lib auto-resolves to ./src/lib in Svelte.
  import { state, connect } from '$lib/state';

  onMount(async () => {
    connect();
  });
</script>

<!--
  We haven't defined the ItemList component (which should
  go in ./src/lib too), but this shows how you can auto-
  subscribe to the store using `$state`.  Every time state
  updates, $state.items changes and this will re-render
-->
<ItemList items={$state.items} />
```

This shows the power of Svelte, SvelteKit's routing, and state management.  You can access state from anywhere in your app - no component hierarchy needed - and it's super easy to use within your components.

Svelte is incredibly powerful and developer efficient.  Give it at try!