![minified-size](https://img.shields.io/bundlephobia/minzip/value-enhancer)
![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)
A tiny library to enhance value with reactive wrapper.
Docs: https://value-enhancer.js.org
Install
npm add value-enhancer
Features
- Plain reactivity.
Without the implicit-cast Proxy magic like Vue Reactivity and MobX. - Single-layer shallow reactivity.
It does not convert the value with Object.defineProperty
nor Proxy
. Keeping everything as plain JavaScript value makes it easier to work with other libraries and easier for the JavaScript engine to optimize. - Safe and fast.
It solves multi-level derivation issue (exist in Svelte Stores) with smart lazy value evaluation.
- Explicit.
Reactive objects are easy to tell since their types are different from normal objects. Subscription also requires explicit dependency declaration which reduce the work of repetitive dynamic dependency collection in Proxy implementations. - Simple DX.
No hidden rules for getting or setting values. What you see is what you get. - Bundle size and Performance.
By carefully defining scope and choosing right features that balance usability and performance, less work needed to be done in value-enhancer
which makes it smaller and faster.
Quick Q&A
Why not MobX?
MobX is cleverly designed to make properties magically reactive. But after using it in many of our large projects people started to complain about this implicit behavior. It is hard to tell if a property is reactive unless enforcing some kind of code style rules. Rules of MobX are easy to be broken especially for new team members.
MobX does not work well with other libraries. It could break other libraries if you forget to exclude instances from other libraries from making observable. toJS
is also needed if data is passed to other libraries.
MobX also prints error when it sees another version of MobX in the global. It is not a good choice for making SDK or library that will be delivered into customer's environment.
In my opinion, the vision of MobX has to be implemented as language-level features, otherwise it will create all kinds of compatibility issues. Svelte, SolidJS and even Vue are heading towards the compiler direction now, looking forward to the next generation of MobX.
In value-enhancer
reactive Vals and plain JavaScript values are easy to distinguish since they have different types. The values of reactive Vals are still plain JavaScript values so it works fine with other libraries. It is small and does not have global variable issues.
Why not Vue Reactivity?
Vue3 brings Reactivity as standalone APIs. It is beautifully designed and I had learned a lot from its source code.
But even though it is made standalone, it is still very Vue centered. Many extra works related to Vue Components are added under the hood.
Vue supports lazy deep reactive conversion. It converts plain JavaScript values into reactive values which means it also suffers from the same issues of MobX.
It is a good choice if you are choosing the Vue ecosystem. The implementation of value-enhancer
absorbs many optimization strategies from Vue Reactivity while staying framework agnostic.
Why not RxJS?
I love RxJS and the reactive paradigm behind it. But the goal of RxJS is to compose asynchronous or callback-based code. It is not optimized for state management.
It also requires you to write code in a pipe-able way which may not be acceptable for everyone.
What about React Hooks?
The signature of combine
and derive
in value-enhancer
may look familiar to those who have used React hooks.
import { useMemo } from "react";
const derived = useMemo(() => source + 1, [source]);
I really like the explicit dependency declaration, but in React it is error-prone since people keep forgetting adding or removing dependencies. The React team even made a exhaustive-deps
linter rule for this.
value-enhancer
solves this by absorbing the RxJS-style callbacks.
import { val, derive, combine } from "value-enhancer";
const source$ = val(1);
console.log(source$.value);
const derived$ = derive(source$, source => source + 1);
console.log(derived$.value);
const combined$ = combine(
[source$, derived$],
([source, derived]) => source + derived
);
console.log(combined$.value);
Since the type of reactive objects are different from its values, it is hard to have mismatched dependencies inside the transform
function.
value-enhancer
can be used in React with a super-simple hook use-value-enhancer
.
Svelte Stores?
Svelte offers excellent support for Observables. Svelte store is one of the simplest implementations. The code is really neat and clean.
Svelte store works well for simple cases but it also leaves some edge cases unresolved. For example, when derived
a list of stores, the transform function could be invoked with intermediate states.
Svelte also adds a $xxx
syntax for subscribing Observables as values. The compiled code is really simple and straightforward.
value-enhancer
is compatible with Svelte Store contract. It can be used in Svelte just like Svelte stores.
SolidJS?
SolidJS "create"s are like React hooks but with saner signatures. It is also thoughtfully optimized for edge cases.
A thing that one may feel odd in SolidJS is accessing reactive value by calling it as function. value-enhancer
keeps the xxx.value
way to access reactive value which I think should be more intuitive.
It also suffers from implicit magic issues like MobX and Vue where you ended up using something like mergeProps
and splitProps
.
value-enhancer
is compatible with SolidJS using from
.
Preact Signals?
Preact recently released Signals which shares similar ideas with value-enhancer
. It is like signals of SolidJS but without the odd function-like value accessing. It flushes reactions top-down then bottom-up like Vue and value-enhancer
.
The Preact team also took a step further to support writing Signals directly within TSX. This offers Svelte-like neat coding experience.
const count = signal(0);
<p>Value: {count.value}</p>
<p>Value: {count}</p>
<input value={count} />
But it also uses Vue-like magic to collect effects.
const counter = signal(0);
effect(() => {
console.log(counter.value);
});
It might seem clean at first but it's not a self-consistent solution either. You'll probably meet weird issues and find workarounds like signal.peek()
which is error-prone.
const counter = signal(0);
const effectCount = signal(0);
effect(() => {
console.log(counter.value);
effectCount.value = effectCount.peek() + 1;
});
This issue does not exist in value-enhancer
because we do not collect dependencies implicitly.
Usage
import { val, combine, derive } from "value-enhancer";
const count$ = val(2);
console.log(count$.value);
count$.set(3);
console.log(count$.value);
count$.subscribe(count => console.log(`subscribe: ${count}`));
count$.reaction(count => console.log(`reaction: ${count}`));
count$.set(3);
count$.value = 4;
const derive$ = derive(count$, count => count * 3);
console.log(derived$.value);
derived$.subscribe(derived => console.log(`derived: ${derived}`));
const combined$ = combine(
[count$, derived$],
([count, derived]) => count + derived
);
console.log(combined$.value);
combined$.subscribe(combined => console.log(`combined: ${combined}`));
count$.set(5);
Legacy
(v1)