preact-signals-utils
TypeScript icon, indicating that this package has built-in type declarations

0.1.0 • Public • Published

Preact Signals Utilities

Reactive utilities for @preact/signals. Inspired by the various signal utilities included with Solid.js.

Installation

# npm
npm install preact-signals-utils

# yarn
yarn add preact-signals-utils

# pnpm
pnpm install preact-signals-utils

Usage

resource

import { signal } from "@preact/signals"
import { resource } from "preact-signals-utils"

const characterID = signal(1)

async function fetchUser(id: number): Promise<Character> {
    let data = await fetch(`https://swapi.dev/api/people/${id}/`)
    let json = await data.json()
    return { id, ...json }
}

const character = resource(characterID, fetchUser)

character.value // Character | undefined | never
character.loading.value // boolean
character.error.value // unknown | undefined
character.latest.value // Character | undefined | never
character.state.value // "unresolved" | "pending" | "ready" | "refreshing" | "errored"
character.refetch(/* options?: { key: Key } */)

collection

import { Identifiable, collection, BrowserStorageEngine } from "preact-signals-utils"

interface Character extends Identifiable { ... }

const characters = collection<Character>({
    storage: new BrowserStorageEngine({ path: "characters" }), // StorageEngine
    // initialValue?: Character[]
})

characters.isEmpty.value // boolean
characters.value // Character[]

characters.add(character.value)
characters.add([newCharacter1, newCharacter2])

characters.delete(oldCharacter)
characters.delete([oldCharacter1, oldCharacter2])

characters.clear()
import { collection, BrowserStorageEngine } from "preact-signals-utils"

interface Menu {
    name: string
    price: number
}

const menus = collection<Menu>({
    storage: new BrowserStorageEngine({ path: "characters" }), // StorageEngine
    cacheIdentifier: "name", // keyof Menu
    // initialValue?: Menu[]
})

mapArray

import type { ReadonlySignal } from "@preact/signals"
import { mapArray } from "preact-signals-utils"

declare const menus: ReadonlySignal<Menu[]>
const prices = mapArray(menus, menu => menu.value.price)
prices.value // number[]

selector

import { signal, effect } from "@preact/signals"
import { selector, mapArray } from "preact-signals-utils"

const heroes = signal([
    { id: 1, name: "Wonder Woman" },
    { id: 2, name: "Superman" },
    { id: 3, name: "Batgirl" },
    { id: 4, name: "The Flash" },
])
const selected = signal<number | null>(null)
const isSelected = selector(selected)

const colors = mapArray(heroes, hero => {
    return isSelected(hero.value.id).value ? "purple" : "black"
})

effect(() => console.log(colors.value))
// => ["black", "black", "black", "black"]

selected.value = 1
// => ["purple", "black", "black", "black"]

selected.value = 4
// => ["black", "black", "black", "purple"]

Combined Example

This is an example of a view model for a component utilizing most of the aforementioned signal utilities to create one unified object that can be used in any component.

// view-model.ts
import { signal, computed, effect } from "@preact/signals"
import type { ReadonlySignal } from "@preact/signals"
import { resource, collection, selector } from "preact-signals-utils"

export interface Character {
    id: number
    name: string
}

export class StarWarsViewModel {
    characterID = signal(1)
    character = resource(this.characterID, this.fetchUser)

    characterStore = collection<Character>({
        storage: new BrowserStorageEngine("star-wars-characters"),
    })
    sortedCharacters = computed(() => {
        return this.characterStore.value.sort((lhs, rhs) => rhs.id - lhs.id)
    })

    selectedCharacter = signal<number | null>(null)
    isSelected = selector(this.selectedCharacter)

    constructor() {
        effect(() => {
            if (this.character.value) {
                this.characterStore.add(this.character.value)
            }
        })
    }

    onInput = (event: JSX.TargetedEvent<HTMLInputElement, Event>) => {
        let value = event.currentTarget.value
        if (value.length) this.characterID.value = parseInt(value)
    }

    async fetchUser(id: number): Promise<Character> {
        let data = await fetch(`https://swapi.dev/api/people/${id}/`)
        let { name } = await data.json()
        return { id, name }
    }

    selectCharacter(character: ReadonlySignal<Character>) {
        this.selectedCharacter.value = character.value.id
    }
}

Here is how you would use this view model in your component:

import { useMemo } from "preact/hooks"
import { useComputed } from "@preact/signals"
import { Show, For } from "preact-signals-utils"
import { StarWarsViewModel } from "./view-model"

export function StarWarsCharacters() {
    const model = useMemo(() => new StarWarsViewModel(), [])

    return (
        <>
            <input
                type="number"
                placeholder="Enter Numeric ID"
                min="1"
                value={model.characterID}
                onInput={model.onInput}
            />

            <Show when={model.character.loading}>Loading...</Show>

            <For each={model.sortedCharacters}>
                {character => (
                    <Character
                        character={character}
                        onSelect={() => model.selectCharacter(character)}
                    />
                )}
            </For>
        </>
    )
}

function Character({ character, onSelect }) {
    const style = useComputed(() => ({
        cursor: "pointer",
        color: model.isSelected(character.value.id).value ? "red" : "black",
    }))

    const text = useComputed(() => JSON.stringify(character.value, null, 4))

    return (
        <p style={style} onClick={onSelect}>
            <code>{text}</code>
        </p>
    )
}

License

Published under the MIT License.

Readme

Keywords

none

Package Sidebar

Install

npm i preact-signals-utils

Weekly Downloads

1

Version

0.1.0

License

MIT

Unpacked Size

102 kB

Total Files

56

Last publish

Collaborators

  • roonieone