Svelte 5 (Preview) State Wrappers

September 20, 2023

Svelte 5 (Preview) State Wrappers

Table of Contents

Disclaimer

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.

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

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

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.

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

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

ts
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));
		}
	};
}