Svelte 5 (Preview) State Wrappers

Published: September 20, 2023

Svelte 5 (Preview) State Wrappers

Table of Contents

Disclaimer

Svelte 5 is a work in progress. It is not ready for production use. This post is currently just a set of ideas based on previews of the new features. It will be updated as the features are finalized.

Writeable / Ref

Svelte 4 had a simple writeable store which allowed you to create a global shared state easily. Svelte 5 does not yet have such a method, but it is super easy to create one using the new $state hook. This ref method is almost functionally equivalent in usage to the writeable store from Svelte 4, except that it uses a getter/setter instead of the magic $ syntax. And that is because the original writeable is an observable, whereas $state is a signal. Check out this article to learn the difference. To summarize, an observable is subscribed to, whereas a signal is just a value.

class Ref<T> {
	_value: T;
	constructor(initial: T) {
		let state = $state(initial);
		this._value = state;
	}

	get value(): T { return this._value }
	set value(newState: T) { this._value = newState }
}

function ref<T>(initial: T) {
	return new Ref(initial);
}

Getters and setters on classes are more performant than on objects, but if you prefer the object syntax, you can use this version instead.

function ref<T>(initial: T) {
	let state = $state(initial);
	return {
		get value() { return state },
		set value(newState: T) { state = newState }
	};
}

Runify Nested Objects

This utility function is useful for creating reactive objects from JSON data. The idea was first created by this Twitter user and modified by @PaoloRicciuti. I added TypeScript types and fixed an issue with arrays and effects. Some array issues still remain.

export function deepRef<T extends object>(obj: T): T {
	if(typeof obj === 'object') {
		let rune = $state(obj);
		for(let key in rune) {
			rune[key] = deepRef(rune[key] as T);
		}
		return new Proxy(rune, {
			get(target, prop) {
				if (typeof target[prop] === "object" && "$" in target[prop]) {
					return target[prop].$;
				}
				if(isNaN(parseInt(prop))) return Reflect.get(...arguments);
				return target[prop];
			},
			set(target, prop, value) {
				if (Array.isArray(value)) {
					target[prop] = deepRef(value as T);
					return true;
				}
				if (typeof target[prop] === "object" && "$" in target[prop]) {
					target[prop].$ = value;
					return true;
				}
				if(isNaN(parseInt(prop))) return Reflect.set(...arguments);
				target[prop] = value;
				return true;
			}
		});
	}

  let rune = $state(obj);
  return {
    get $() {
      return rune;
    },
    set $(val) {
      rune = val;
    }
  }
}

Solid.js createMutable() Approach

@intrnl on the Svelte Discord, created an alternative based on the createMutable() function from Solid. This version is more complex, but it resolves some of the issues with the above version.

History

This is a simple utility function that can be used to create a history object, which tracks the current value and allows you to undo and redo changes to the value. Set the maxLength to 0 to disable the history limit.

function createHistory<T>(initial: T, maxLength = 50) {
	let history = $state<T[]>([initial]);
	let index = $state(0);

	return {
		get current() { return history[index] },
		set current(newState: T) {
			history.splice(index + 1, history.length - index, newState);
			if (maxLength > 0) history = history.slice(Math.max(0, history.length - maxLength), history.length);
			index = history.length - 1;
		},
		get entries() { return history },
		get index() { return index },
		get canUndo() { return index > 0 },
		get canRedo() { return index < history.length - 1 },
		undo() { index = Math.max(0, index - 1) },
		redo() { index = Math.min(history.length - 1, index + 1) }
	};
}

Fetch Wrapper

This is a simple utility function that can be used to create a fetcher object. It is a simple wrapper around the fetch API that provides reactivity for Svelte 5.

function createFetcher(url) {
	let result = $state(null);
	let loading = $state(false);
	let error = $state(null);

	const controller = new AbortController();

	async function runFetch() {
		loading = true;
		try {
			const resp = await fetch(url, { signal: controller.signal });
			result = await resp.json();
		} catch {
			error = "Add error message here!";
			result = null;
		}

		loading = false;
	}

	$effect(async () => {
		runFetch();
		return controller.abort;
	});

	return {
		get result() { return result },
		get loading() { return loading },
		get error() { return error },
		abort(reason) { controller.abort(reason) },
		refetch: runFetch
	};
}

Websocket Wrapper

This is a simple utility function that can be used to create a websocket object. It is a simple wrapper around the WebSocket API that provides reactivity for Svelte 5.

export default function createSocket<T>(
	url: string | URL,
	options?: {
		name?: string;
		protocols?: string | string[];
		onOpen?: (event: Event) => void;
		onError?: (error: Event) => void;
		onMessage?: (data: T, requests: T[], event: Event) => void;
	}
) {
	let requests = $state<T[]>([]);
	let latest = $derived(requests.at(requests.length - 1));
	let websocket: WebSocket | null = null;

	$effect(() => {
		websocket = new WebSocket(url, options?.protocols)

		websocket.onopen = (event) => {
			console.log(`${options?.name || "Websocket"} connected!`);
			if (options?.onOpen) options.onOpen(event);
		};

		websocket.onerror = (error) => {
			console.log(`${options?.name || "Websocket"} error:`, error);
			if (options?.onError) options.onError(error);
		};

		websocket.onmessage = (event) => {
			const data = JSON.parse(event.data) as T;
			requests = requests.concat(data);
			if (options?.afterMessage) options.onMessage(data, requests, event);
		};

		return () => {
			if (websocket) websocket.close();
		}
	});

	return {
		get requests() { return requests },
		get latest() { return latest },
		close() {
			if (websocket) websocket.close();
		},
		send(data: T) {
			if (!websocket) throw new Error("No websocket connection");
			if (websocket.readyState !== websocket.OPEN) throw new Error("No websocket connection");
			websocket.send(JSON.stringify(data));
		}
	};
}