Skip to content

tronikelis/solid-swr

Repository files navigation

solid-swr

Swr ideaology brought to solid


Introduction

Quote from vercel's SWR for react:

The name “SWR” is derived from stale-while-revalidate, a HTTP cache invalidation strategy popularized by HTTP RFC 5861. SWR is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.

With SWR, components will get a stream of data updates constantly and automatically. And the UI will be always fast and reactive.

Features

  • 💙 Built for solid
  • ⚡ Blazingly fast with reconciled solid stores and zero* extra hook allocation
  • ♻️ Reusable and lightweight data fetching
  • 📦 Optional built-in cache and request deduplication
  • 🔄 Local mutation (optimistic UI)
  • 🔥 0 dependencies
  • 😉 And much more!

For v4 docs readme

Install

pnpm i solid-swr

Quick start

import { useSwr, SwrProvider, Store } from "solid-swr"
import { LRU, createCache } from "solid-swr/cache"

function App() {
    const { v, mutate, revalidate } = useSwr(() => "/api/user/2")

    const onClick = () => {
        mutate({ name: "user2" })
        // if you need to revalidate
        revalidate()
    }

    return (
        <div onClick={onClick}>
            {v().isLoading}
            {v().data}
        </div>
    )
}

function Root(props) {
    return (
        <SwrProvider value={{ store: new Store(createCache(new LRU())) }}>
            {props.children}
        </SwrProvider>
    )
}

Explanation

Hook returns 3 values which you can destructure:

  • v: function that indexes into solid store
  • mutate: basically setStore but scoped to the key
  • revalidate: call fetcher again (not guaranteed to be called due to deduplication), this function returns the result of the fetch call

Ideaology

Here I want to share some context about the ideaology of this library and swr in general

The "key"

The key is a string which is used as an ID into some server side state

const key = "api/user/id"
useSwr(() => key)

The key is almost always used as a url to a backend resource

Solid store as a source of truth

Everything is stored in a solid store, i.e. isLoading, data, err, etc... All hooks / utilities, talk to a single object through a Store interface

This way, solid handles the syncing of changes to listeners, thus:

  1. we avoid implementing syncing cough cough solid-swr@v4
  2. we avoid duplicating large amounts of js objects, again cough cough solid-swr@v4
  3. And most importantly, this gives us O(1) time complexity to call useSwr
Simple graph explaining what I've said

Behavior can be customized through public core APIs

In v5, useSwr is a core hook, meaning that it is simple and meant to be extended

In fact, useSwr is just a simple function that uses other public apis:

  • createRevalidator
  • createMutator

An excerpt from useSwr as of the time I'm writing this

const runWithKey = <T extends (k: string) => any>(fn: T): ReturnType<T> | undefined => {
    const k = key();
    if (!k) return;
    return fn(k);
};

const revalidator = createRevalidator(ctx);
const mutator = createMutator(ctx);

// as you can see, revalidate is just a convenience method to call revalidtor
const revalidate = () => runWithKey(k => revalidator<D, E>(k));
// mutate is exactly the same
const mutate = (payload: Mutator<D>) => runWithKey(k => mutator<D, E>(k, payload));

If you at any time need a revalidator, or a mutator, just use createRevalidator or createMutator, or create new abstractions with these 2, just like pretty much all hooks in this lib

Core

This is the most important part of the library which contains all core utilities for you to manage server side state with swr

Store

This is by far THE most important part of the library

The store is a solid.js store object with the key string as the key

export type SolidStore = {
    [key: string]: StoreItem | undefined;
};

Each item in the store contains these properties:

  • data: a generic value
  • err: a generic value
  • isLoading: a boolean

For more info I suggest you looking at the src/store.ts everything is there

Cache

A separate user-provided cache is used to remove items from the store

Connect your cache with the store like so:

export type StoreCache = {
    /** item has been inserted into store */
    insert: (key: string, onTrim: OnTrimFn) => void;
    /** item has been looked up */
    lookup: (key: string, onTrim: OnTrimFn) => void;
};
const store = new Store({
    lookup: (key, onTrim) => lru.get(key),
    insert: (key, onTrim) => lru.set(key, true, onTrim)
})

solid-swr provides this behavior ootb

import { LRU, createCache } from "solid-swr/cache"

new Store(createCache(new LRU()))

The onTrim is how the store connects to the cache, call onTrim(key) to remove a key from the solid store

In the case above when lru tries to set a key it will trim the cache, thus removing (if needed) a key

Methods

Store can be mutated / read with its public methods

  • lookupOrDef: gets the correct item or returns default
  • update: update store while reconciling data
  • updateProduce: update store with solid produce util

createRevalidator

import { createRevalidator } from "solid-swr"

Create a function that revalidates (calls fetcher) a key

This function also deduplicates requests, so when you call it, the actual fetcher call is not guaranteed

createMutator

import { createMutator } from "solid-swr"

Create a function that can change any key in the store

useSwr

import { useSwr } from "solid-swr"

Hook that uses createRevalidator and createMutator to create the swr behavior that we all love

Returns:

  • mutate: createMutator scoped to a key
  • revalidate: createRevalidator scoped to a key
  • v: a function that indexes into a solid store

Options

src/core.ts
export type SwrOpts<D = unknown, E = unknown> = {
    store: Store;

    fetcher: (key: string, { signal }: FetcherOpts) => Promise<unknown>;
    /** gets direct store references (don't mutate) */
    onSuccess: (key: string, res: D) => void;
    /** gets direct store references (don't mutate) */
    onError: (key: string, err: E) => void;

    /** gets direct references to response (don't mutate) */
    onSuccessDeduped: (key: string, res: D) => void;
    /** gets direct reference to response (don't mutate) */
    onErrorDeduped: (key: string, err: E) => void;
};

Passing options

Options can be passed either to a useSwr hook instance or with SwrProvider

Reading options

Options can be read with useSwrContext

Extra

import * as extra from "solid-swr/extra"

All of the recipes shown here could have been created by using the #core utils

If you have come up with an awesome recipe that's not shown here, I would love to add it to solid-swr

I encourage you to take a look at src/extra.tx to get more context about inner workings of these recipes

useSwrFull

This is similar to the default swr in solid-swr@v4

Basically it is core hook with extra options:

export type SwrFullOpts = {
    keepPreviousData: boolean;
    revalidateOnFocus: boolean;
    revalidateOnOnline: boolean;
    fallback: Fallback;
    refreshInterval: number;
};

Setting these options is the same as in core but with useSwrFull* utils

useMatchMutate

Uses createMutator to mutate multiple keys at once

useMatchRevalidate

Uses createRevalidator to revalidate multiple keys at once

This hook also skips revalidating items from the store which do not have any hooks attached to them, this is known by looking at _mountedCount number

useSwrInfinite

Used for infinite loading, returns an array of accessors into correct store index

import { useSwrInfinite } from "solid-swr/store"

const { data } = useSwrInfinite((index, prevData) => `https://example.com?page=${index}`)

// here we get first item, then we access the store with second ()
// then get the actual `data` that we need
const firstItemData = data()[0]().data