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

v2.0.2

Published

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

Downloads

28

Readme

@diffx/react

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

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

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

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

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

// 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/react';
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/react';
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.

  • 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/react';
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:

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

useDiffx() react hook

useDiffx(getterFunc) is a React hook that enables reading the state in Diffx and re-rendering when it changes.

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

export default function App() {
    const count = useDiffx(() => counterState.count);

    function incrementCounter() {
        setState('Increment counter', () => counterState.count++);
    }

    return (
        <div>
            <div>Current click count: {count}</div>
            <button onClick={incrementCounter}>Increment to {count + 1}</button>
        </div>
    );
}

useDiffx(getterFunc, options) can be provided a second options argument to configure the watching.

  • 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(() => counterState.count, {
    /**
     * Whether to start with emitting the current value of the getter.
     *
     * Default: `true`
     */
    emitInitialValue: true,
    /**
     * 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/react';
import { clickCounter } from './createState-example';

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

// stop watching
unwatchFunc();

A watcher is allowed to change the state when triggered.

import { watchState, setState } from '@diffx/react';
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

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

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

watchState(
    () => [clickCounter.count, usersState.names],
    ([clickCount, names]) => console.log(clickCount, names)
);

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

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