Resource

NPM
v0.3.1

#Installation

npm install @solid-primitives/resource
yarn add @solid-primitives/resource
pnpm add @solid-primitives/resource

#Readme

A collection of composable primitives to augment createResource

#How to use it

Here's an example of all of them combined:

// abort signal will abort the resource in-flight if it takes more then 10000ms
const [signal, abort] = makeAbortable({ timeout: 10000 });

const fetcher = (url: string) => fetch(url, { signal: signal() }).then(r => r.json());

// cached fetcher will not be called if something for the same URL is still in cache
const [cachedFetcher, invalidate] = makeCache(fetcher, { storage: localStorage });

// works with createResource, or any wrapping API with the same interface
const [data, { refetch }] = createResource(address, fetcher);
const aggregatedData = createAggregated(data);

Obviously, you're not limited to resources using fetch. You can use them on any resource you want.

#createAggregated

Aggregates the output of a resource:

const aggregated: Accessor<T> = createAggregated(
  resource: Resource<T>, initialValue?: T | U
);
const pages = createAggregated(currentPage, []);
  • null will not overwrite undefined
  • if the previous value is an Array, incoming values will be appended
  • if any of the values are Objects, the current one will be shallow-merged into the previous one
  • if the previous value is a string, more string data will be appended
  • otherwise the incoming data will be put into an array

Objects and Arrays are re-created on each operation, but the values will be left untouched, so <For> should work fine.

#createDeepSignal

Usually resources in Solid.js are immutable. Every time the resource updates, every subscriber of it is updated. Starting with Solid.js 1.5, createResource allows to receive a function returning something akin to a signal in options.storage. This allows to provide the underlying storage for the resource in order to change its reactivity. This allows to add fine-grained reactivity to resources so that you can subscribe to nested properties and only trigger updates as they actually occur:

// this adds fine-grained reactivity to the contents of data():
const [data, { refetch }] = createResource(fetcher, { storage: createDeepSignal });

Warning if your resource is a deep signal, you can no longer rely on reactive changes to the base signal. If you want to combine this with createAggregated, you will need to wrap the resource to either call deepTrack or read one of the reactive parts that you know for certain will change every time:

import { deepTrack } from "@solid-primitives/deep";

const [data] = createResource(source, fetcher, { storage: createDeepSignal });
const aggregated = makeAggregated(() => deepTrack(data()));

#makeAbortable

Orchestrates AbortController creation and aborting of abortable fetchers, either on refetch or after a timeout, depending on configuration:

// definition
const [
  signal: AbortSignal,
  abort: () => void,
  filterErrors: <E>(err: E) => E instanceof AbortError ? void : E
] = makeAbortable({
  timeout?: 10000,
  noAutoAbort?: true,
});

// usage
const fetcher = (url: string) => fetch(
  url, { signal: signal() }
).then(r => r.json(), filterErrors);
  • The signal function always returns a signal that is not yet aborted; if noAutoAbort is not set to true, calling it will also abort a previous signal, if present
  • The abort callback will always abort the current signal
  • If timeout is set, the signal will be aborted after that many Milliseconds
  • The filterErrors function can be used to filter out abort errors

#createAbortable

This function does exactly the same as makeAbortable, but also automatically aborts on cleanup. Only use within a reactive scope.

#makeCache

Creates a caching fetcher, with the ability to persist the cache, to invalidate entries and manage expired entries:

const [
  fetcher: ResouceFetcher<S, T>,
  invalidate: ((source?: S) => void) & { all: () => void },
  expired: Accessor<{ source: S, data: T }>
] =
  makeCache(
    fetcher: ResourceFetcher<S, T>,
    options?: {
      cache?: Record<string, { source: S, data: T }>,
      expires?: number | (entry: { source: S, data: T }) => number,
      storage?: Storage,
      storageKey?: string,
    }
  );

Wraps the fetcher to use a cache. Returns the wrapped fetcher, an invalidate callback that requires the source to invalidate the request and a signal accessor with the last automatically invalidated request.

Can be customized with the following optional options:

  • cache - allows to use a local cache instead of the global one
  • expires - allows to define a custom timeout; either accepts a number or a function that receives an object with source and data of the request and returns a number in Milliseconds
  • serialize - a tuple [serialize, deserialize] used for persistence, default is [JSON.stringify, JSON.parse]
  • sourceHash - a function receiving the source (true if none is used) and returns a hash string
  • storage - a storage like localStorage to persist the cache over reloads
  • storageKey - the key which is used to store the cache in the storage

⚠ the default sourceHash function works with simple types as well as Headers and Maps, but will fail on recursive objects and will throw on Symbols. It should work for simple RequestInit type objects and is pretty small.

#makeRetrying

Creates a fetcher that retries multiple times in case of errors.

const fetcher = makeRetrying(url => fetch(url).then(r => r.json()), { retries: 5, delay: 500 });

Receives the fetcher and an optional options object and returns a wrapped fetcher that retries on error after a delay multiple times.

The optional options object contains the following optional properties:

  • delay - number of Milliseconds to wait before retrying; default is 5s
  • retries - number of times a request should be repeated before giving up throwing the last error; default is 3 times

#Recipes: the missing pieces

Maybe you have considered using TanStack Query instead of this collection and if you have a lot of complex requirements around requests, this might even be a better fit. However, if only a few crucial pieces of it are missing for you, here are some recipes to fill them in:

#Query keys

Nothing stops you from using keys as Source for your fetch request through makeCache. Those keys will be serialized as identifiers for caching, too. All you need is a translation table from keys to the actual request in the fetcher.

#Network mode

Just filter your source with isOnline from the connectivity package:

import { createConnectivitySignal } from "@solid-primitives/connectivity";

const isOnline = createConnectivitySignal();
const source = () => isOnline() && url();
const [data] = createResource(source, url => fetch(url));

#Window focus refetching

The createEventListener primitive from the event-listener pacakge comes in helpful to do that:

import { createEventListener } from "@solid-primitives/event-listener";

const [data, { refetch }] = createResource(() => fetch("url"));
createEventListener(document, "visibilitychange", () => document.hidden || refetch());

You could also augment this code with the scheduled package to throttle the refetch calls not to happen again sooner than after 5s:

import { createEventListener } from "@solid-primitives/event-listener";
import { throttle } from "@solid-primitives/scheduled";

const [data, { refetch }] = createResource(() => fetch("url"));
const runRefetch = throttle(refetch, 5000);
createEventListener(document, "visibilitychange", () => document.hidden || runRefetch());

#Stopping a refetch interval when hidden or offline

If you are polling, this approach might come useful:

import { createConnectivitySignal } from "@solid-primitives/connectivity";
import { createEventSignal } from "@solid-primitives/event-listener";
import { createTimer } from "@solid-primitives/timer";

const [data, { refetch }] = createResource(() => fetch("url").then(r => r.json()));
const [setPaused] = createTimer(refetch, 5000, setInterval);
const visibilityChange = createEventSignal(document, "visibilitychange");
const isOnline = createConnectivitySignal();
createEffect(() => setPaused((visibilityChange(), document.hidden) || !isOnline());

#Mutations

In TanStack Query, mutations are requests that mutate data on the server, so we are less interested in the answer and more in the timing. Fortunately, Solid already comes with a mutate action in the createResource return value:

const [todos, { mutate, refetch }] = createResource(getTodos);

const addTodo = todo => {
  [mutation] = createResource(() => addTodo(todo));
  const current = todos();
  // optimistic update
  mutate(todos => [...current, todo]);
  // refetch after mutating on the server or error
  createRoot(done =>
    createEffect(() => {
      if (mutation.error || !mutation.loading) {
        refetch();
        done();
      }
    }),
  );
  return () => mutation.state;
};

#Scroll Restoration

This is already covered in @solidjs/router.

#Polling

Just use an interval with refetch; ideally, also use makeAbortable.

#Demo

Live Site

You may view a working example of our resource primitives here: https://primitives.solidjs.community

#Changelog

See CHANGELOG.md