@diffx/rxjs
v3.0.2
Published
Diffx is a state management library that focuses on three things:
Downloads
40
Maintainers
Readme
@diffx/rxjs
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
--> @diffx/react
Vue.js
--> @diffx/vue
Svelte
--> @diffx/svelte
Angular
--> @diffx/angular
RxJS
--> @diffx/rxjs
No framework --> @diffx/core
Installation
npm install @diffx/rxjs
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/rxjs';
setDiffxOptions({ devtools: true });
import { setDiffxOptions } from '@diffx/rxjs';
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/rxjs';
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 statepersistent
- Persist the latest snapshot of this state and automatically use that as the initial state. Setting it tofalse
will exclude the state from persistence, even though it is globally set totrue
insetDiffxOptions
.
Default:false
persistenceLocation
- Location for persisting this particular state - e.g.window.sessionStorage
.
Default:false
import { setDiffxOptions, createState } from '@diffx/rxjs';
// 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/rxjs';
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/rxjs';
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
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 rxjsObservable
.onDone
- a function that receives the result ofasyncMutatorFunc
as an argument, and is free to change the state.onError
- a function that receives the error fromasyncMutatorFunc
as an argument, and is free to change the state.
import { createState, setState } from '@diffx/rxjs';
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:
To avoid repeating yourself, it can be beneficial to wrap setState
in a function that can be reused.
import { createState, setState } from '@diffx/rxjs';
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/rxjs';
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 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/rxjs';
import { clickCounter } from './createState-example';
observeState(() => clickCounter.count); // --> observable
import { observeState, setState } from '@diffx/rxjs';
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".
The tag can be hovered/clicked for more information about its trigger origin.
import { observeState } from '@diffx/rxjs';
import { clickCounter } from './createState-example';
observeState(() => clickCounter.count > 5)
.subscribe(isGreaterThanFive => {
console.log(isGreaterThanFive); // --> true/false
});
import { observeState } from '@diffx/rxjs';
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/rxjs';
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).
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()
.
Displays the current state at the selected diff.
Displays the stack trace for the code that led to this state change.
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.
For places where setState()
has been used inside setState()
, the left pane will display a nested view with colors
used for displaying nesting depth.
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.
If a watchState()
runs setState()
, the left pane will display a watcher
tag to indicate that the change was
triggered.
The watcher
tag can be hovered to see which state change triggered it and clicked to find the state change.
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.
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