Runed Documentation

repository·main·Indexed 23 days ago

https://github.com/svecosystem/runed

A utility library providing enhanced functionality for applications using Svelte Runes. It offers core utilities like isMounted and useEventListener, as well as SvelteKit-specific utilities such as useSearchParams via the runed/kit subpath. Key features include the AnimationFrames class for reactive animation loops, a type-safe Context wrapper for Svelte context management, a Debounced class for reactive debounced state, and utilities for tracking element size and bounding rectangles.

Tokens
38.4K
Snippets
102
Records
175
Agent score
82%

What's inside Runed

  1. What is Runed?

    main

    Runed is a collection of utility functions and classes designed specifically for Svelte 5. It leverages Svelte Runes to provide a set of primitives that simplify common tasks, reduce boilerplate, and enhance the core reactivity system of Svelte 5.

    Key features include:

    • Reactivity First: Designed to handle reactive state and side effects seamlessly using Svelte 5's reactivity model.
    • Type Safety: Full TypeScript support for improved developer experience and error catching.
    • Consistency: A unified set of APIs and behaviors across all utilities.
    • Enhancement: Runed acts as a natural extension to Svelte's core functionality rather than a replacement.
  2. Custom serialization with Zod Codecs

    main

    For advanced control over how values are converted between URL strings and JavaScript types, use Zod codecs (Zod v4.1.0+). Codecs allow you to define bidirectional transformations via decode (URL string $\rightarrow$ JS type) and encode (JS type $\rightarrow$ URL string).

    Common Use Cases:

    • Compact IDs: Storing numbers as base36 strings.
    • Unix Timestamps: Storing dates as integers for smaller URLs.
    • Custom Date Formats: Any non-standard string format.
    • Complex Conversions: Mapping IDs to full objects or vice-versa.

    Codecs work automatically with both useSearchParams on the client and validateSearchParams on the server.

    import { z } from "zod";
    
    // Example: Unix timestamp codec
    const unixTimestampCodec = z.codec(
    	z.coerce.number(), // Input: number from URL string
    	z.date(), // Output: Date object in your app
    	{
    		decode: (timestamp) => new Date(timestamp * 1000),
    		encode: (date) => Math.floor(date.getTime() / 1000)
    	}
    );
    
    // Using it in a schema
    const searchSchema = z.object({
    	createdAfter: unixTimestampCodec.default(new Date("2024-01-01")),
    });
  3. How `resource` handles multiple dependencies

    main

    To track multiple reactive dependencies, pass an array of source getters to the resource function. The fetcher function will then receive an array containing the current values of those dependencies.

    <script lang="ts">
    	const results = resource([() => query, () => page], async ([query, page]) => {
    		const res = await fetch(`/api/search?q=${query}&page=${page}`);
    		return res.json();
    	});
    </script>
  4. Use wildcard handlers for fallback events

    main

    You can define a special state named "*" to act as a fallback. If an event is sent that is not handled by the current state, the FSM will check the "*" state for a handler before discarding the event.

    import { FiniteStateMachine } from "runed";
    type MyStates = "on" | "off";
    type MyEvents = "toggle" | "emergency";
    
    const f = new FiniteStateMachine<MyStates, MyEvents>("off", {
    	off: {
    		toggle: "on"
    	},
    	on: {
    		toggle: "off"
    	},
    	"*": {
    		emergency: "off"
    	}
    });
    
    // This will trigger the emergency handler in the wildcard state
    f.send("emergency");
  5. Manage `onClickOutside` listeners with `start` and `stop`

    main

    The onClickOutside function returns a control object that allows you to programmatically enable or disable the listener. This is useful for components like <dialog> elements where you only want to listen for outside clicks while the component is visible.

    Control Methods

    • start(): Enables the listener.
    • stop(): Disables the listener.
    • enabled: A reactive, read-only property indicating the current status of the listener.

    To use controlled mode, set the immediate option to false in the configuration object.

    <script lang="ts">
    	import { onClickOutside } from "runed";
    
    	let dialog = $state<HTMLDialogElement>()!;
    
    	const clickOutside = onClickOutside(
    		() => dialog,
    		() => {
    			dialog.close();
    			clickOutside.stop();
    		},
    		{ immediate: false }
    	);
    
    	function openDialog() {
    		dialog.showModal();
    		clickOutside.start();
    	}
    
    	function closeDialog() {
    		dialog.close();
    		clickOutside.stop();
    	}
    </script>
    
    <button onclick={openDialog}>Open Dialog</button>
    <dialog bind:this={dialog}>
    	<div>
    		<button onclick={closeDialog}>Close Dialog</button>
    	</div>
    </dialog>
  6. How to use the Context class for type-safe data sharing

    main

    The Context class is a type-safe wrapper around Svelte's Context API. It allows you to pass data through a component tree without prop drilling. The workflow consists of three steps:

    1. Define: Create a Context instance with a specific type and a debug name.
    2. Set: Call .set(value) in a parent component during initialization.
    3. Get: Access the value in child components using .get() or .getOr(fallback).

    Important: All Context methods (set, get, getOr, exists) must be called during component initialization. They cannot be used inside event handlers or asynchronous callbacks.

    import { Context } from "runed";
    
    // 1. Define
    export const myTheme = new Context<"light" | "dark">("theme");
  7. Understand the reactivity scope of `useSearchParams`

    main

    useSearchParams provides top-level reactivity only. This means that while direct property assignments trigger URL updates, nested mutations do not.

    ✅ What works (Direct assignment)

    Directly assigning a value to a property on the returned object will trigger a URL update:

    <script>
    	const params = useSearchParams(schema);
    
    	// These trigger URL updates
    	params.page = 2;
    	params.filter = "active";
    	params.config = { theme: "dark", size: "large" };
    	params.items = [...params.items, newItem];
    </script>

    ❌ What doesn't work (Nested mutations)

    Mutating properties inside objects or arrays, or using array methods, will not trigger URL updates:

    <script>
    	const params = useSearchParams(schema);
    
    	// These DON'T trigger URL updates
    	params.config.theme = "dark"; // Nested object property
    	params.items.push(newItem); // Array method
    	params.items[0].name = "updated"; // Array item property
    	delete params.config.oldProp; // Property deletion
    </script>
  8. Handle Date serialization in search parameters

    main

    You can control how Date parameters are serialized in URLs using two methods:

    Method 1: Using dateFormat in createSearchParamsSchema Set dateFormat to 'date' for YYYY-MM-DD or 'datetime' (default) for full ISO8601.

    Method 2: Using dateFormats option in useSearchParams This works with any validation library (Zod, Valibot, etc.). Pass an object mapping field names to 'date' or 'datetime'.

    • 'date' format: Serializes as YYYY-MM-DD. Parsed as a Date object with time set to midnight UTC.
    • 'datetime' format: Serializes as full ISO8601. Preserves exact time information.
    // Option 1: In schema
    const schema = createSearchParamsSchema({
    	birthDate: { type: "date", default: new Date("1990-01-15"), dateFormat: "date" }
    });
    
    // Option 2: In useSearchParams options
    const params = useSearchParams(zodSchema, {
    	dateFormats: {
    		birthDate: "date",
    		createdAt: "datetime"
    	}
    });
  9. Persisting complex objects with PersistedState

    main

    When using PersistedState with complex objects, only plain structures are deeply reactive. This includes arrays, plain objects, and primitive values.

    Note: Class instances are not deeply reactive. Modifying a property on a class instance will NOT persist the change. To persist changes to a class instance, you must re-assign the entire instance to .current.

    const persistedArray = new PersistedState("foo", ["a", "b"]);
    persistedArray.current.push("c"); // This will persist the change
    
    const persistedObject = new PersistedState("bar", { name: "Bob" });
    persistedObject.current.name = "JG"; // This will persist the change
    
    class Person {
    	name: string;
    	constructor(name: string) {
    		this.name = name;
    	}
    }
    const persistedComplexObject = new PersistedState("baz", new Person("Bob"));
    // The following will NOT persist:
    persistedComplexObject.current.name = "JG"; 
    // The following WILL persist:
    persistedComplexObject.current = new Person("JG");
  10. Implement custom cleanup in a resource fetcher

    main

    The fetcher function provides an onCleanup callback. Use this to register functions that should run before the next fetch occurs (e.g., closing a WebSocket or an EventSource). This prevents resource leaks when dependencies change rapidly.

    <script lang="ts">
    	const stream = resource(
    		() => streamId,
    		async (id, _, { signal, onCleanup }) => {
    			const eventSource = new EventSource(`/api/stream/${id}`);
    			onCleanup(() => eventSource.close());
    
    			const res = await fetch(`/api/stream/${id}/init`, { signal });
    			return res.json();
    		}
    	);
    </script>