@diffx/svelte

2.0.2 • Public • Published

@diffx/svelte

Diffx is a state management library that focuses on three things:

  • Make it easy to learn and use
  • Get rid of boilerplate
  • Make great devtools

Key features

🤏 Small API and a very compact syntax
🔍 Tracks the reason behind changes to the state
🔧 Devtools that track:
     - what, when, where and why state changed
     - async start/resolution
     - nested changes
     - changes triggered by watchers
💾 Built in persistence
📝 Written in Typescript, inferring your types

Supported frameworks

react logo React --> @diffx/react
vue logo Vue.js --> @diffx/vue
svelte logo Svelte --> @diffx/svelte
angular logo Angular --> @diffx/angular
rxjs logo RxJS --> @diffx/rxjs
No framework --> @diffx/core

Installation

npm install @diffx/svelte

And install the devtools browser extension for a better development experience (view documentation).

Usage

Configure Diffx

setDiffxOptions(options) is used to configure which global features to enable for Diffx. Should be run before any code interacts with Diffx.

  • options - an options object that configures how Diffx works internally
import { setDiffxOptions } from '@diffx/svelte';

setDiffxOptions({ devtools: true });
Show all options
import { setDiffxOptions } from '@diffx/svelte';

setDiffxOptions({
    /**
     * Enable viewing the state history in devtools.
     * Not recommended for use in a production environment.
     * If set to true, `createDiffs` will also be implicitly true.
     *
     * Default: false
     */
    devtools: false,
    /**
     * Store a stack-trace with every diff if `createDiffs` is enabled.
     * Will be displayed in devtools to help with tracking down
     * which code is making state changes.
     *
     * NOT recommended in production environment since creating stack traces is a slow operation!
     *
     * Default: false
     */
    includeStackTrace: false,
    /**
     * Persist the latest snapshot of all states and automatically use that as the initial state
     *
     * Default: false
     */
    persistent: false,
    /**
     * Location for storing persistent state.
     * E.g. localStorage or sessionStorage
     *
     * Default: null
     */
    persistenceLocation: null,
    /**
     * Whether to record all diffs of the state in-memory.
     *
     * Default: false
     **/
    createDiffs: false,
    /**
     * Max nesting depth.
     *
     * If a loop of setState <--> watchState is accidentally created, it will run off and crash
     * (and potentially crash the main thread). To avoid this, a max nesting depth can be set.
     *
     * Default: 100
     */
    maxNestingDepth: 100
});

Create state

createState(namespace, state) is used to create state in Diffx.

  • namespace - a string which is used as the key when storing the state in the state tree. The namespace must be unique.
  • state - an object which contains the initial state
import { createState } from '@diffx/svelte';

export const usersState = createState('users state', { names: [] });
export const clickCounter = createState('click counter', { count: 0 });

console.log(clickCounter.count); // --> 0

You can create as many states as you like and access them as regular objects to read their values.

Configure persistence

createState(..., ..., options)

  • options- optional settings for this particular state
    • persistent - Persist the latest snapshot of this state and automatically use that as the initial state. Setting it to false will exclude the state from persistence, even though it is globally set to true in setDiffxOptions.
      Default: false

    • persistenceLocation - Location for persisting this particular state - e.g. window.sessionStorage.
      Default: false

import { setDiffxOptions, createState } from '@diffx/svelte';

// this enables persistence for all states globally
setDiffxOptions({
    persistent: true,
    persistenceLocation: sessionStorage
})

// this disables persistence for a specific state (if it's enabled globally)
export const clickCounter = createState('click counter', { count: 0 }, { persistent: false });

// this state is persisted in accordance with the global settings in `setDiffxOptions`
export const clickCounter = createState('click counter', { count: 0 });

// this state is persisted in localStorage instead of the globally defined persistenceLocation
export const clickCounter = createState('click counter', { count: 0 }, { persistenceLocation: localStorage });

Update state

setState(reason, mutatorFunc) is used to wrap changes to the state.

  • reason - a string which explains why the state was changed. Will be displayed in the devtools extension for easier debugging.
  • mutatorFunc - a function that wraps all changes to the state.
import { setState } from '@diffx/svelte';
import { clickCounter } from './createState-example';

setState('increment the counter', () => clickCounter.count++);
Synchronous usage

Since Diffx is proxy-based, it will keep track of anything happening within setState().
Multiple states can be changed within one setState():

import { setState } from '@diffx/svelte';
import { clickCounter, usersState } from './createState-example';

setState('Change the counter and add a user', () => {
    clickCounter.count++;
    if (clickCounter.count > 2) {
        clickCounter.count = 200;
    }
    usersState.names.push('John');
})

This will also create an entry in the devtools
devtools entry screenshot

Asynchronous usage

setState(reason, asyncMutatorFunc, onDone [, onError]) is used to make asynchronous changes to the state.

  • reason - a string which explains why the state was changed. Will be displayed in the devtools extension for easier debugging.
  • asyncMutatorFunc - a function that is free to change the state, and returns a Promise.
  • onDone - a function that receives the result of asyncMutatorFunc as an argument, and is free to change the state.
  • onError - a function that receives the error from asyncMutatorFunc as an argument, and is free to change the state.
import { createState, setState } from '@diffx/svelte';
import { fetchUsersStateFromServer } from './some-file';

export const usersState = createState('usersState-status', {
    isFetching: false,
    names: [],
    fetchErrorMessage: ''
});

setState(
    'fetch and update usersState',
    () => {
        // set state before the async work begins
        usersState.fetchErrorMessage = '';
        usersState.names = [];
        usersState.isFetching = true;
        // return the async work
        return fetchUsersStateFromServer();
    },
    result => {
        // the async work succeeded
        usersState.names = result;
        usersState.isFetching = false;
    },
    error => {
        // the async work failed
        usersState.fetchErrorMessage = error.message;
        usersState.isFetching = false;
    }
);

The asyncMutatorFunc and its resolution with onDone or onError will be tracked in the devtools:

onDone

async onDone in devtools

onError

async onError in devtools

Nesting setState() inside setState()

To avoid repeating yourself, it can be beneficial to wrap setState in a function that can be reused.

import { createState, setState } from '@diffx/svelte';
import { usersState } from './createState-example';

export function addUser(name) {
    setState('Add user', () => usersState.names.push(name));
}

To make the state history more readable, the usage of the wrapped setState above can be used inside a setState providing a reason for the changes and grouping them.

// in some other file
import { setState } from '@diffx/svelte';
import { addUser } from './example-above';

setState('PeopleComponent: User clicked "Save usersState"', () => {
    addUser('John');
    addUser('Jenny');
});

This nesting will be displayed in the devtools as an indented hierarchical list, clarifying why "Add user" happened:
nesting in devtools

Nesting can go as many levels deep as desired, making it easy to see who did what and why, and at the same time making it easy to discover reusable compositions of setState.

Why can't I directly modify the state?

By having the freedom to change state from anywhere in the codebase, state can quickly get out of control and be difficult to debug if there is no human-readable reasoning behind why a change was made.
To ensure that the usage experience stays developer friendly, easy to debug, and help with identifying which code needs refactoring, Diffx enforces the use of setState since it groups changes and allows the developer to specify a reason for the changes.

Any changes made to the state outside of setState() will throw an error.

import { clickCounter } from './createState-example';

clickCounter.count++; // this will throw an error

Read state

Directly

When rendering state from the store as is (without any projections), it can be accessed directly:

<script>
    import { setState } from '@diffx/svelte';
    import { clickCounter } from './createState-example';

    export function increment() {
        setState('increment', () => clickCounter.count++);
    }
</script>

<main>
    <button on:click={increment}>Increment counter to {clickCounter.count + 1}</button>
    <div>Current count: {clickCounter.count}</div>
</main>

Projected

useDiffx(getterFunc) returns a readonly value that will be kept updated.

  • getterFunc - a function that returns state or a projection of state.
<script>
    import { setState, useDiffx } from '@diffx/svelte';
    import { clickCounter } from './createState-example';

    export const isDivisibleByThree = useDiffx(() => clickCounter.count % 3 === 0);

    export function increment() {
        setState('increment', () => clickCounter.count++);
    }
</script>

<main>
    <button on:click={increment}>Increment counter to {clickCounter.count + 1}</button>
    <div>Current count: {clickCounter.count}</div>
    <div>Is divisible by three: {$isDivisibleByThree}</div>
</main>
Configuring useDiffx()

useDiffx(getterFunc, options) returns a readonly value that will be kept updated.

  • getterFunc - a function that returns state or a projection of state.
  • options - an options object describing how the state should be watched
const count = useDiffx(() => clickCounter.count, {
    /**
     * Whether to emit each change to the state during .setState (eachValueUpdate),
     * the current state after each .setState and .setState nested within it (eachSetState),
     * or to only emit the final state after the outer .setState function has finished running (setStateDone).
     *
     * This can be used to optimize rendering if there e.g. is a need to render every value as it updates in Diffx.
     *
     * Default: `setStateDone`
     */
    emitOn: 'eachSetState' | 'setStateDone' | 'eachValueUpdate',
    /**
     * Custom comparer function to decide if the state has changed.
     * Receives newValue and oldValue as arguments and should return `true` for changed
     * and `false` for no change.
     */
    hasChangedComparer: (newValue, oldValue) => true / false
});

Watch state

watchState(stateGetter, options) is used for watching the state and being notified/reacting when it changes.

  • stateGetter - a function which returns the state(s) to be watched
  • callback - a callback that will be called the next time the watched state changes

watchState is useful when creating "background services" that watches the state and reacts when it changes.

import { watchState } from '@diffx/svelte';
import { clickCounter } from './createState-example';

const unwatchFunc = watchState(
    () => clickCounter,
    (newValue, oldValue) => {
        console.log('counter changed to', newValue.count);
    }
);

// stop watching
unwatchFunc();
Using setState() inside watchState()

A watcher is allowed to change the state when triggered.

import { watchState, setState } from '@diffx/svelte';
import { clickCounter } from './createState-example';

watchState(
    () => clickCounter.count === 5,
    countIsFive => {
        if (!countIsFive) return;
        setState('Counter has the value 5, so I added another user', () => {
            usersState.names.push('Jenny');
        });
    }
);

This will also be tracked in the devtools and tagged with "watcher".
devtools watcher example

The tag can be hovered/clicked for more information about its trigger origin.
devtools watcher hover example

Watching projections
import { watchState } from '@diffx/svelte';
import { clickCounter } from './createState-example';

watchState(
    () => clickCounter.count > 5,
    isAboveFive => console.log(isAboveFive)
);
Watching multiple states
import { watchState } from '@diffx/svelte';
import { clickCounter, usersState } from './createState-example';

watchState(
    () => [clickCounter.count, usersState.names],
    ([clickCount, names]) => console.log(clickCount, names)
);
Controlling how state is watched

To have fine-grained control over how the state is watched, the second argument can be an options object instead of a callback.

import { watchState } from '@diffx/svelte';
import { clickCounter } from './createState-example';

const unwatchFunc = watchState(() => clickCounter, {
    /**
     * Whether to emit the current value of the watched item(s).
     *
     * Default: `false`
     */
    emitInitialValue: false,
    /**
     * Callback called with the final state after the outermost `.setState` function has finished running.
     * This is the default behavior when using a callback instad of an options object.
     */
    onSetStateDone: (newValue, oldValue) => '...',
    /**
     * Callback called with the current state after each `.setState` has finished running
     * (including each .setState wrapped in .setState)
     */
    onEachSetState: (newValue, oldValue) => '...',
    /**
     * Callback for each change to the state during `.setState`.
     */
    onEachValueUpdate: (newValue, oldValue) => '...',
    /**
     * Custom comparer function to decide if the state has changed.
     * Receives newValue and oldValue as arguments and should return `true` for changed
     * and `false` for no change.
     *
     * Default: Diffx built in comparer
     */
    hasChangedComparer: (newValue, oldValue) => 'true or false',
    /**
     * Whether the watcher should automatically stop watching after the first changed value has
     * been emitted.
     *
     * Default: false
     */
    once: false
});

// stop watching
unwatchFunc();

Destroy state

destroyState(namespace) is used for removing state from diffx.

  • namespace - the namespace (string) to destroy

Any watchers of the destroyed state will not be automatically unwatched.

import { destroyState } from '@diffx/svelte';

destroyState('click counter');

Devtools browser extension

Installation

The extension can be installed through the Chrome web store.

Features

Diffx devtools is made to give insights into

  • Why state was changed
  • Which state was changed
  • When did it change
  • What caused the change
Overview

It will show up as a tab in the browser devtools when it detects that the page is using Diffx and debugging has been enabled (see setDiffxOptions).

Devtools location

The left pane displays a list of changes (diffs) to the state along with their reason.
The right pane displays the Diff, State and Stacktrace (if stacktrace has been enabled in setDiffxOptions).

Diff tab

Displays the difference between each change made by setState().

Diff tab preview

State tab

Displays the current state at the selected diff.

State tab preview

Stacktrace tab

Displays the stack trace for the code that led to this state change.

Stacktrace tab preview

State namespace indicators

The dots in the left tab indicate which state was changed with their color, can be hovered to view the namespace and clicked to filter the list by that state.

State type hints

Visualizing nested setState

For places where setState() has been used inside setState(), the left pane will display a nested view with colors used for displaying nesting depth.

Nested setState preview

Tracing async setState

For async operations done with setState(), the left pane will display an async tag where the operation starts, and a resolve/reject tag where the async operation finished.
These tags are highlighted with a color to make it easier to spot which operations belong together and are also clickable to filter by.

setState preview

Tracing state changed in watchState

If a watchState() runs setState(), the left pane will display a watcher tag to indicate that the change was triggered.

watchState tracing preview 1

The watcher tag can be hovered to see which state change triggered it and clicked to find the state change.

watchState tracing preview 2

To see where in the code the watcher was run, enable includeStackTrace in setDiffxOptions and open the Stacktrace tab for the entry tagged with the watcher.

Highlight/filter changes to a specific value

The Highlight and Filter button can be used to find the state changes that affected a specific value.

highlight/filter preview

Do I need a state management library?

A lot of projects start out with keeping state localized. When the project grows and requirements change, some of that state usually gets refactored to become shared state. That might work well for a while, but as the project grows even further, it can become a real mental burden to keep track of the current state and how it came to be. The author of the code might not feel this way, but the next developer to join the project is almost guaranteed to have a hard time keeping up with it. This is usually when developers will reach for a library to aid with state management.

If you foresee a project that will grow in size over time, and/or other developers will join, it might be a good idea to use a well documented and inspectable way to manage state.

Why Diffx?

There are a lot of great state management libraries out there.

  • Some focus on a rigid structure, suitable for large teams that want predictable code patterns, often at the cost of writing a lot of boilerplate.
  • Some provide the same ease of use as local state, often at the cost of having less context which might make it more difficult to debug.

Diffx tries to be the best of both worlds by

  • making it easy to provide context/intent behind any changes, which in turn makes it easy to reason about how a specific state came to be. It makes the state self-documenting.
  • compactness comparable to local state
  • offloading the responsibility to stay in control over to the library/devtools

There are a heap of great choices out there, and the library you end up using will probably stay in your project for a long time. Diffx is a tool - I recommend you to look into several of the popular ones before you decide which is the best fit for your project.

Credits and thanks

  • Thanks to the team behind Vue.js for making a great framework and the @vue/reactive package this project depends on.
  • Thanks to Benjamine, the creator of jsondiffpatch which this project uses for creating diffs.
  • Thanks to all developers teaming together to share their creations with others

Package Sidebar

Install

npm i @diffx/svelte

Weekly Downloads

2

Version

2.0.2

License

MIT

Unpacked Size

27.2 kB

Total Files

5

Last publish

Collaborators

  • jbjorge