npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@diffx/angular

v3.0.1

Published

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

Downloads

31

Readme

@diffx/angular

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/angular

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

Setup

Fix angular change detection and the async pipe

Angular has the concept of code running inside zones, and anything running outside a zone will not trigger change detection.

To ensure observables returned from Diffx are run in the correct zone, import zone-patch-rxjs in your polyfills.ts file after your import of zone.

import 'zone.js/dist/zone';
import 'zone.js/dist/zone-patch-rxjs'; // <--- This thing right here

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/angular';

setDiffxOptions({ devtools: true });
import { setDiffxOptions } from '@diffx/angular';

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/angular';

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.

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/angular';

// 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/angular';
import { clickCounter } from './createState-example';

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

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/angular';
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

setState(reason, asyncMutatorFunc, onDone [, onError]) is used to make asynchronous changes to the state (and enhances tracking of async state in Diffx devtools).

  • 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 rxjs Observable.
  • 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/angular';
import { fetchUsersFromServer } from './some-file';

export const usersState = createState('users state', {
    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 fetchUsersFromServer();
    },
    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:

async onDone in devtools

async onError in devtools

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

import { createState, setState } from '@diffx/angular';
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/angular';
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.

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

Observe state

observeState(stateGetter) is used for creating an observable of the state.

  • stateGetter - a function which returns the state(s) to be observed
import { observeState } from '@diffx/angular';
import { clickCounter } from './createState-example';

observeState(() => clickCounter.count); // --> observable
import { observeState, setState } from '@diffx/angular';
import { clickCounter, usersState } from './createState-example';

observeState(() => clickCounter.count)
    .pipe(
    	filter(count => count === 5),
        take(1)
    )
    .subscribe(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

import { observeState } from '@diffx/angular';
import { clickCounter } from './createState-example';

observeState(() => clickCounter.count > 5)
  .subscribe(isGreaterThanFive => {
  	console.log(isGreaterThanFive); // --> true/false
  });
import { observeState } from '@diffx/angular';
import { clickCounter, usersState } from './createState-example';

observeState(() => [clickCounter.count, usersState.names])
  .subscribe(([count, names]) => {
  	console.log(count) // --> number
  });

To have fine-grained control over how the state is observed, an options object can be provided as the second argument.

import { observeState } from '@diffx/rxjs';
import { clickCounter } from './createState-example-above';

const observable = observeState(() => clickCounter.count, {
    /**
     * Whether to start with emitting the current value of the observed item(s).
     *
     * Default: `false`
     */
    emitInitialValue: false,
    /**
     * 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).
     *
     * 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.
     *
     * Default: Diffx built in comparer
     */
    hasChangedComparer: (newValue, oldValue) => 'true / false'
});

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/angular';

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

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).

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

Diff tab preview

Displays the current state at the selected diff.

State tab preview

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

Stacktrace tab preview

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

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

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

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.

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