Skip to content

Provides writable/readable stores that can 'zoom' into the part of the store value (so called "nested store").

License

Notifications You must be signed in to change notification settings

igrep/svelte-store-tree

Repository files navigation

svelte-store-tree

npm version License CI

Current status: Experimental.

Provides writable/readable stores that can 'zoom' into a part of the store value (so-called "nested stores"). It enables us to manage the state of the app in a single object while keeping the independence of every child component.

Example

import { writableTree, Refuse, into, isPresent } from 'svelte-store-tree';
import type { WritableTree } from 'svelte-store-tree';

type SomeRecord = {
  id: number;
  name: string;
  contact: {
    phone: string;
    urls: string[];
  };
  favoriteColor: Color | undefined;
};

type Color = [number, number, number];

// Create a `WritableTree`
const someRecord: WritableTree<SomeRecord> = writableTree({
  id: 0,
  name: 'Y. Y',
  contact: {
    phone: '+81-00-0000-0000',
    urls: [
      'https://the.igreque.info',
      'https://github.com/igrep',
    ],
  },
  favoriteColor: undefined
});

// Subscribe as an ordinary store.
someRecord.subscribe((newUser) => {
  console.log('Updated the user', newUser);
});

// `zoom` with the `into` Accessor;
//    Create a store that subscribes only a specific field of the object
const name = someRecord.zoom(into('name'));
const contact = someRecord.zoom(into('contact'));
const favoriteColor = someRecord.zoom(into('favoriteColor'));

name.subscribe((newName) => {
  console.log('Updated the name', newName);
});
contact.subscribe((newContact) => {
  console.log('Updated the contact', newContact);
});
favoriteColor.subscribe((newColor) => {
  console.log('Updated the color', newColor);
});

// We can apply `zoom` deeper:
const urls = contact.zoom(into('urls'));

// Notifies the subscribers of `someRecord`, `contact`, and `urls`.
// ** Changes are propagated only to the direct subscribers, and the ancestors'. **
// ** Not to the the siblings' to avoid extra rerendering of the subscribing components. **
urls.update((u) => [...u, 'https://twitter.com/igrep']);

// If your record contains a union type, the `choose` method is useful.
// Pass a function that returns a `Refuse` (a unique symbol provided by this library)
// if the value doesn't satisfy the condition.
const favoriteColorNonUndefined =
  favoriteColor.choose((color) => color ?? Refuse);

// Now, favoriteColorNonUndefined is typed as `WritableTree<Color>`,
// while favoriteColor is `WritableTree<Color | undefined>`.

// As a shortcut for a nullable type, svelte-store-tree provides
// the `isPresent` function used with `choose`:
const favoriteColorNonUndefined2 = favoriteColor.choose(isPresent);

favoriteColorNonUndefined.subscribe((newColor) => {
  console.log('Updated the color', newColor);
});

// Notifies the subscribers of `someRecord`, `favoriteColor`, and `favoriteColorNonUndefined`.
favoriteColor.set([0xC0, 0x10, 0x10]);

// Notifies the subscribers of `someRecord`, and `favoriteColor` (not `favoriteColorNonUndefined`).
favoriteColor.set(undefined);

Working Example App

Run on CodeSandbox

Example App running on CodeSandbox

Installation

$ npm install --save svelte-store-tree

API

// Core API
export function writableTree<P>(
  value: P,
  start: StartStopNotifier<P> = noop,
): WritableTree<P>;

export function readableTree<P>(
  value: P,
  start: StartStopNotifier<P> = noop,
): ReadableTree<P>

/// Types related to the Core API
export type StoreTreeCore<P> = {
  zoom<C>(accessor: Accessor<P, C>): WritableTree<C>;
  zoomNoSet<C>(readChild: (parent: P) => C | Refuse): ReadableTree<C>;
  choose<P_ extends P>(readChild: (parent: P) => P_ | Refuse): WritableTree<P_>;
};
export type ReadableTree<P> = Readable<P> & StoreTreeCore<P>;
export type WritableTree<P> = Writable<P> & StoreTreeCore<P>;

export const Refuse: unique symbol = Symbol();
export type Refuse = typeof Refuse;

/// Utility function to help the `StoreTreeCore.prototype.choose` method
export function isPresent<P>(parent: P): NonNullable<P> | Refuse;

// Accessor API
export class Accessor<P, C> {
  constructor(readChild: (parent: P) => C | Refuse, writeChild: (parent: P, newChild: C) => void);
  readChild: (parent: P) => C | Refuse;
  writeChild: (parent: P, newChild: C) => void;
  and<GC>(other: Accessor<C, GC>): Accessor<P, GC>;
};

/// Various Utility Accessors
export function into<P, K extends keyof P>(key: K): Accessor<P, P[K]>;
export function intoMap<K extends string | number | symbol, V>(key: K): Accessor<Map<K, V>, V>;

About

Provides writable/readable stores that can 'zoom' into the part of the store value (so called "nested store").

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published