Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/elbywan/hyperactiv
A super tiny reactive library. :zap:
https://github.com/elbywan/hyperactiv
computed es6-proxies javascript properties react reactive
Last synced: 2 days ago
JSON representation
A super tiny reactive library. :zap:
- Host: GitHub
- URL: https://github.com/elbywan/hyperactiv
- Owner: elbywan
- License: mit
- Created: 2017-12-31T15:13:10.000Z (almost 7 years ago)
- Default Branch: master
- Last Pushed: 2024-09-27T14:04:27.000Z (about 2 months ago)
- Last Synced: 2024-10-25T01:33:46.582Z (22 days ago)
- Topics: computed, es6-proxies, javascript, properties, react, reactive
- Language: JavaScript
- Homepage:
- Size: 1.73 MB
- Stars: 437
- Watchers: 12
- Forks: 25
- Open Issues: 6
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
- awesome-tiny-js - hyperactiv - 4 functions to make objects observable and listen to changes, <img align="top" height="24" src="./img/hyperactiv.svg"> (State Managers / Signals)
README
Hyperactiv
A super tiny reactive library. ⚡️
## Description
Hyperactiv is a super small (~ 1kb minzipped) library which **observes** object mutations and **computes** functions depending on those changes.
In other terms whenever a property from an observed object is **mutated**, every function that **depend** on this property are **called** right away.
Of course, Hyperactiv **automatically** handles these dependencies so you **never** have to explicitly declare anything. ✨
----
#### Minimal working example
```js
import hyperactiv from 'hyperactiv'
const { observe, computed } = hyperactiv// This object is observed.
const observed = observe({
a: 1,
b: 2,
c: 0
})// Calling computed(...) runs the function and memorize its dependencies.
// Here, the function depends on properties 'a' and 'b'.
computed(() => {
const { a, b } = observed
console.log(`a + b = ${a + b}`)
})
// Prints: a + b = 3// Whenever properties 'a' or 'b' are mutated…
observed.a = 2
// The function will automagically be called.
// Prints: a + b = 4observed.b = 3
// Prints: a + b = 5observed.c = 1
// Nothing depends on 'c', so nothing will happen.
```## Demo
**[Paint demo](https://elbywan.github.io/hyperactiv/paint)**
**[React store demo](https://elbywan.github.io/hyperactiv/todos)**
**[React hooks demo](https://github.com/elbywan/hyperactiv-hooks-demo)**
## Setup
```bash
npm i hyperactiv
``````html
```
## Import
**Hyperactiv is bundled as an UMD package.**
```js
// ESModules
import hyperactiv from 'hyperactiv'
``````js
// Commonjs
const hyperactiv = require('hyperactiv')
``````js
// Global variable
const { computed, observe, dispose } = hyperactiv
```## Usage
#### 1. Observe object and arrays
```js
const object = observe({ one: 1, two: 2 })
const array = observe([ 3, 4, 5 ])
```#### 2. Define computed functions
```js
let sum = 0// This function calculates the sum of all elements,
// which is 1 + 2 + 3 + 4 + 5 = 15 at this point.
const calculateSum = computed(() => {
sum = [
...Object.values(object),
...array
].reduce((acc, curr) => acc + curr)
})// A computed function is called when declared.
console.log(sum) // -> 15
```#### 3. Mutate observed properties
```js
// calculateSum will be called each time one of its dependencies has changed.object.one = 2
console.log(sum) // -> 16
array[0]++
console.log(sum) // -> 17array.unshift(1)
console.log(sum) // -> 18
array.shift()
console.log(sum) // -> 17
```#### 4. Release computed functions
```js
// Observed objects store computed function references in a Set,
// which prevents garbage collection as long as the object lives.
// Calling dispose allows the function to be garbage collected.
dispose(calculateSum)
```## Add-ons
#### Additional features that you can import from a sub path.
- **[hyperactiv/react](https://github.com/elbywan/hyperactiv/tree/master/src/react)**
*A simple but clever react store.*
- **[hyperactiv/http](https://github.com/elbywan/hyperactiv/tree/master/src/http)**
*A reactive http cache.*
- **[hyperactiv/handlers](https://github.com/elbywan/hyperactiv/tree/master/src/handlers)**
*Utility callbacks triggered when a property is mutated.*
- **[hyperactiv/classes](https://github.com/elbywan/hyperactiv/tree/master/src/classes)**
*An Observable class.*
- **[hyperactiv/websocket](https://github.com/elbywan/hyperactiv/tree/master/src/websocket)**
*Hyperactiv websocket implementation.*
## Performance
This repository includes a [benchmark folder](https://github.com/elbywan/hyperactiv/tree/master/bench) which pits `hyperactiv` against other libraries.
**Important: the benchmarked libraries are not equivalent in terms of features, flexibility and developer friendliness.**
While not the best in terms of raw performance `hyperactiv` is still reasonably fast and I encourage you to have a look at the different implementations to compare the library APIs. [For instance there is no `.get()` and `.set()` wrappers when using `hyperactiv`](https://github.com/elbywan/hyperactiv/blob/master/bench/layers.mjs#L361).
**Here are the raw results: _(100 runs per tiers, average time ignoring the 10 best & 10 worst runs)_**
![bench](./docs/bench.png)
> Each tier nests observable objects X (10/100/500/1000…) times and performs some computations on the deepest one. This causes reactions to propagate to the whole observable tree.
_**Disclaimer**: I adapted the code from [`maverickjs`](https://github.com/maverick-js/observables/tree/main/bench) which was itself a rewrite of the benchmark from [`cellx`](https://github.com/Riim/cellx#benchmark). I also wrote some MobX code which might not be the best in terms of optimization since I am not very familiar with the API._
## Code samples
#### A simple sum and a counter
```js
// Observe an object and its properties.
const obj = observe({
a: 1,
b: 2,
sum: 0,
counter: 0
})// The computed function auto-runs by default.
computed(() => {
// This function depends on a, b and counter.
obj.sum = obj.a + obj.b
// It also sets the value of counter, which is circular (get & set).
obj.counter++
})// The function gets executed when computed() is called…
console.log(obj.sum) // -> 3
console.log(obj.counter) // -> 1
obj.a = 2
// …and when a or b are mutated.
console.log(obj.sum) // -> 4
console.log(obj.counter) // -> 2
obj.b = 3
console.log(obj.sum) // -> 5
console.log(obj.counter) // -> 3
```#### Nested functions
```js
const obj = observe({
a: 1,
b: 2,
c: 3,
d: 4,
totalSum: 0
})const aPlusB = () => {
return obj.a + obj.b
}
const cPlusD = () => {
return obj.c + obj.d
}// Depends on a, b, c and d.
computed(() => {
obj.totalSum = aPlusB() + cPlusD()
})console.log(obj.totalSum) // -> 10
obj.a = 2
console.log(obj.totalSum) // -> 11
obj.d = 5
console.log(obj.totalSum) // -> 12
```#### Chaining computed properties
```js
const obj = observe({
a: 0,
b: 0,
c: 0,
d: 0
})computed(() => { obj.b = obj.a * 2 })
computed(() => { obj.c = obj.b * 2 })
computed(() => { obj.d = obj.c * 2 })obj.a = 10
console.log(obj.d) // -> 80
```#### Asynchronous computations
```js
// Promisified setTimeout.
const delay = time => new Promise(resolve => setTimeout(resolve, time))const obj = observe({ a: 0, b: 0, c: 0 })
const multiply = () => {
obj.c = obj.a * obj.b
}
const delayedMultiply = computed(// When dealing with asynchronous functions
// wrapping with computeAsync is essential to monitor dependencies.({ computeAsync }) =>
delay(100).then(() =>
computeAsync(multiply)),
{ autoRun: false }
)delayedMultiply().then(() => {
console.log(obj.b) // -> 0
obj.a = 2
obj.b = 2
console.log(obj.c) // -> 0
return delay(200)
}).then(() => {
console.log(obj.c) // -> 4
})
```#### Batch computations
```js
// Promisified setTimeout.
const delay = time => new Promise(resolve => setTimeout(resolve, time))// Enable batch mode.
const array = observe([0, 0, 0], { batch: true })let sum = 0
let triggerCount = 0const doSum = computed(() => {
++triggerCount
sum = array.reduce((acc, curr) => acc + curr)
})console.log(sum) // -> 0
// Even if we are mutating 3 properties, doSum will only be called once asynchronously.
array[0] = 1
array[1] = 2
array[2] = 3console.log(sum) // -> 0
delay(10).then(() => {
console.log(`doSum triggered ${triggerCount} time(s).`) // -> doSum triggered 2 time(s).
console.log(sum) // -> 6
})
```#### Observe only some properties
```js
const object = {
a: 0,
b: 0,
sum: 0
}// Use props to observe only some properties
// observeA reacts only when mutating 'a'.const observeA = observe(object, { props: ['a'] })
// Use ignore to ignore some properties
// observeB reacts only when mutating 'b'.const observeB = observe(object, { ignore: ['a', 'sum'] })
const doSum = computed(function() {
observeA.sum = observeA.a + observeB.b
})// Triggers doSum.
observeA.a = 2
console.log(object.sum) // -> 2// Does not trigger doSum.
observeA.b = 1
observeB.a = 1
console.log(object.sum) // -> 2// Triggers doSum.
observeB.b = 2
console.log(object.sum) // -> 3
```#### Automatically bind methods
```javascript
let obj = new SomeClass()
obj = observe(obj, { bind: true })
obj.someMethodThatMutatesObjUsingThis()
// observe sees all!
```#### This and class syntaxes
```js
class MyClass {
constructor() {
this.a = 1
this.b = 2const _this = observe(this)
// Bind computed functions to the observed instance.
this.doSum = computed(this.doSum.bind(_this))// Return an observed instance.
return _this
}doSum() {
this.sum = this.a + this.b
}
}const obj = new MyClass()
console.log(obj.sum) // -> 3
obj.a = 2
console.log(obj.sum) // -> 4
``````js
const obj = observe({
a: 1,
b: 2,
doSum: function() {
this.sum = this.a + this.b
}
}, {
// Use the bind flag to bind doSum to the observed object.
bind: true
})obj.doSum = computed(obj.doSum)
console.log(obj.sum) // -> 3
obj.a = 2
console.log(obj.sum) // -> 4
```## API
### observe
Observes an object or an array and returns a proxified version which reacts on mutations.
```ts
observe(Object | Array, {
props: String[],
ignore: String[],
batch: boolean,
deep: boolean = true,
bind: boolean
}) => Proxy
```**Options**
- `props: String[]`
Observe only the properties listed.
- `ignore: String[]`
Ignore the properties listed.
- `batch: boolean | int`
Batch computed properties calls, wrapping them in a setTimeout and executing them in a new context and preventing excessive calls.
If batch is an integer greater than zero, the calls will be debounced by the value in milliseconds.- `deep: boolean`
Recursively observe nested objects and when setting new properties.
- `bind: boolean`
Automatically bind methods to the observed object.
### computed
Wraps a function and captures observed properties which are accessed during the function execution.
When those properties are mutated, the function is called to reflect the changes.```ts
computed(fun: Function, {
autoRun: boolean,
callback: Function
}) => Proxy
```**Options**
- `autoRun: boolean`
If false, will not run the function argument when calling `computed(function)`.
The computed function **must** be called **at least once** to calculate its dependencies.
- `callback: Function`
Specify a callback that will be re-runned each time a dependency changes instead of the computed function.
### dispose
Will remove the computed function from the reactive Maps (the next time an bound observer property is called) allowing garbage collection.
```ts
dispose(Function) => void
```### batch
_Only when observables are created with the `{batch: …}` flag_
Will perform accumulated b.ed computations instantly.
```ts
const obj = observe({ a: 0, b: 0 }, { batch: true })
computed(() => obj.a = obj.b)
obj.b++
obj.b++
console.log(obj.a) // => 0
batch()
console.log(obj.a) // => 2
```