DEV Community

Cover image for Build a realtime websocket UI using SvelteKit
Tony Holdstock-Brown
Tony Holdstock-Brown

Posted on • Originally published at inngest.com

Build a realtime websocket UI using SvelteKit

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

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
Enter fullscreen mode Exit fullscreen mode

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

// 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
  });
}
Enter fullscreen mode Exit fullscreen mode

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

<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} />
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (1)

Collapse
 
gevera profile image
Denis Donici

That's a cool sneak peak.. I wish you could go on and get into details.