0.34.0 • Published 4 months ago

spred v0.34.0

Weekly downloads
-
License
MIT
Repository
github
Last release
4 months ago

Spred

npm codecov gzip size

Simple and fast JavaScript reactive programming library.

  • Small. 3 KB minified and gziped. No dependencies
  • Fast. No unnecessary calculations and excellent performance
  • Simple. Small API and autotracking of dependencies
  • Well typed. Written in TypeScript

Basic Example

import { writable, computed, batch } from 'spred';

const formatter = new Intl.DateTimeFormat('en-GB');

const name = writable('Paul');
const instrument = writable('bass');
const birthday = writable('1942-06-18');

const formattedBirthday = computed(() =>
  formatter.format(new Date(birthday()))
);

const greeting = computed(
  () =>
    `Hello. My name is ${name()}, I play ${instrument()} and I was born on ${formattedBirthday()}.`
);

greeting.subscribe((str) => console.log(str));
// > Hello. My name is Paul, I play bass and I was born on 18/06/1942.

batch(() => {
  name('Ringo');
  instrument('drums');
  birthday('1940-07-07');
});
// > Hello. My name is Ringo, I play drums and I was born on 07/07/1940.

All examples on StackBlitz

Installation

npm install spred --save

Signals

Signal is the basic reactive primitive of the library. A signal stores a value and notifies its subscribers when it changes. There are two kinds of signals - writable and computed.

Writable Signals

Writable signals are created with a writable function that takes the initial value of the signal.

import { writable } from 'spred';

const counter = writable(0);

To get the value of the signal, you need to call it without arguments.

/*...*/

console.log(counter()); // 0

To set a new value of the writable signal, you need to pass the value as an argument.

/*...*/

counter(1);
console.log(counter()); // 1

Signal value updates can be subscribed to using the subscribe method. The second argument of the method specifies whether the function should be called immediately after subscribing, and defaults to true. The method returns the unsubscribe function.

/*...*/

const unsub = counter.subscribe((value) =>
  console.log('The value is ' + value)
);

// > The value is 1

counter(2);

// > The value is 2

Computed Signals

Computed signals automatically track their dependencies and recalculate their value when any of the dependencies triggered. A computed signal can be created using the computed function. It takes as its argument a function that calculates the value of the signal and depends only on other signal values.

Computed signals initialize their values lazily. That means that the calculation function triggers only when the signal has at least one subscriber / dependent signal with a subscriber.

import { writable, computed } from 'spred';

const counter = writable(0);
const doubleCounter = computed(() => counter() * 2);

doubleCounter.subscribe((value) => console.log('Double value is ' + value));
// > Double value is 0

counter(1);
// > Double value is 2

Change detection

By default all signals trigger their dependants and subscribers only if its value changes.

import { writable, computed } from 'spred';

const counter = writable(0);
const doubleCounter = computed(() => counter() * 2);

doubleCounter.subscribe((value) => console.log('Double value is ' + value));
// > Double value is 0

counter(0);
// Nothing

counter(1);
// > Double value is 2

Signals use Object.is to compare values. A custom compare function can be passed as a second argument of writable or computed.

/*...*/

const obj = writable({ value: 1 }, (a, b) => a.value === b.value);

obj.subscribe((obj) => console.log('Object value is ' + obj.value));
// > Object value is 1

obj({ value: 1 });
// Nothing

obj({ value: 2 });
// > Object value is 2

Error Handling

Any signal that have subscribers and an exception ocured during the computation will log the error in the console and will not cause recalculation of its dependants. You can pass an exception handler as a third argument of the computed.

import { computed, writable } from 'spred';

const sub = (value) => console.log('The value is ' + value);
const source = writable(0);
const withError = computed(() => {
  if (source() > 10) throw 'bigger than 10';
  return source();
});
const result = computed(withError);

const unsub = result.subscribe(sub);
// > The value is 0

source(11);
// > [X] bigger than 10

unsub();
const withCatchedError = computed(result, null, (e) => e);

withCatchedError.subscribe(sub);
// > The value is bigger than 10

source(20);
// Nothing

source(5);
// > The value is 5

Signal Creation

If you need to create a readonly signal and a separate setter at once, you can use the signal function.

import { signal } from 'spred';

const [counter, setCounter] = signal(0);

counter.subscribe((value) => console.log('The value is ' + value));

// > The value is 0

setCounter(1);

// > The value is 1

Signal As Event Emitter

on function allows to subscribe to a signal updates without immediate subscriber execution.

import { on, signal } from 'spred';

const [click, emitClick] = signal();
const unsub = on(click, () => console.log('CLICK'));

document.addEventListener('click', emitClick);
// [> CLICK] on every document click

Batching Updates

Writable signal updates are immediate and synchronous.

import { batch, computed, on, writable } from 'spred';

const a = writable(0);
const b = writable(0);
const sum = computed(() => a() + b());

sum.subscribe((s) => console.log('a + b = ' + s));
// > a + b = 0

a(1);
// > a + b = 1

b(1);
// > a + b = 2

You can commit several updates as a single transaction using the batch function.

/*...*/

batch(() => {
  a(2);
  b(2);
});
// > a + b = 4

All updates inside subscriber functions are batched by default.

const trigger = writable(0);

on(trigger, () => {
  a(3);
  b(3);
});

trigger(1);
// > a + b = 6

Watching

watch function calls the passed callback immediately and every time the signals it depends on are triggered.

import { batch, watch, writable } from 'spred';

const a = writable('Hello');
const b = writable('World');

watch(() => {
  console.log(`${a()} ${b()}!`);
});
// > Hello World!

batch(() => {
  a('Foo');
  b('Bar');
});
// > Foo Bar!

Lifecycle Hooks

Every signal has several lifecycle hooks that can be subscribed to using special functions.

  • onActivate - emits when the signal becomes active (has at least one subscriber / dependent signal with a subscriber);
  • onDectivate - emits when the signal becomes inactive (doesn't have any subscriber / dependent signal with a subscriber);
  • onUpdate - emits when the signal updates its value;
  • onException - emits when an exception is thrown during the signal computation.

Example on StackBlitz

Effects

Effect is a wrapper for an asynchronous function. It creates auxiliary signals that represent the execution state of the function and its result.

import { effect, watch } from 'spred';

function fetchUser(id: number) {
  return fetch(`https://swapi.dev/api/people/${id}`).then((res) => res.json());
}

const userFx = effect(fetchUser);

Every effect has several signal properties.

  • args - receives arguments of the effect call;
  • data - receives the result of the fulfilled effect;
  • exception - receives the result of the rejected effect;
  • done - receives any result of the effect, both fulfilled and rejected;
  • status - receives status object of the effect;
  • aborted - emits when effect is aborted or reset during execution, including a call when the previous call is still pending.
/*...*/

const { data, status, call } = userFx;

watch(() => {
  let html = '';

  switch (status().value) {
    case 'pending':
      html = 'Loading...';
      break;
    case 'rejected':
      html = 'Something went wrong';
      break;
    case 'fulfilled':
      html = `<pre>${JSON.stringify(data(), null, 2)}</pre>`;
  }

  document.body.innerHTML = html;
});

call(1);

Example on StackBlitz

Integration

Svelte

Spred signals implement Svelte store contract so you don't need any additional package to use them in Svelte apps.

Example on StackBlitz

React

Use the spred-react package.

Example on StackBlitz

References

Big thanks for inspiration to

0.34.0

4 months ago

0.33.0

4 months ago

0.32.5

4 months ago

0.32.4

5 months ago

0.32.2

1 year ago

0.32.1

1 year ago

0.32.0

1 year ago

0.30.0

2 years ago

0.29.0

2 years ago

0.29.1

2 years ago

0.31.2

1 year ago

0.31.1

2 years ago

0.31.0

2 years ago

0.27.1

2 years ago

0.27.0

2 years ago

0.25.1

2 years ago

0.25.0

2 years ago

0.23.2

2 years ago

0.23.1

2 years ago

0.23.0

2 years ago

0.21.1

2 years ago

0.21.0

2 years ago

0.28.0

2 years ago

0.26.0

2 years ago

0.24.0

2 years ago

0.22.0

2 years ago

0.18.11

2 years ago

0.18.10

2 years ago

0.20.1

2 years ago

0.20.0

2 years ago

0.19.0

2 years ago

0.19.1

2 years ago

0.15.1

2 years ago

0.17.0

2 years ago

0.15.2

2 years ago

0.15.3

2 years ago

0.18.9

2 years ago

0.16.3

2 years ago

0.18.2

2 years ago

0.16.4

2 years ago

0.18.3

2 years ago

0.16.5

2 years ago

0.18.4

2 years ago

0.16.6

2 years ago

0.18.5

2 years ago

0.16.7

2 years ago

0.18.6

2 years ago

0.16.8

2 years ago

0.18.7

2 years ago

0.16.9

2 years ago

0.18.8

2 years ago

0.16.0

2 years ago

0.16.1

2 years ago

0.16.2

2 years ago

0.20.3

2 years ago

0.20.2

2 years ago

0.15.0

2 years ago

0.9.4

2 years ago

0.9.3

2 years ago

0.11.0

2 years ago

0.10.1

2 years ago

0.12.0

2 years ago

0.11.1

2 years ago

0.10.2

2 years ago

0.13.0

2 years ago

0.12.1

2 years ago

0.14.0

2 years ago

0.13.1

2 years ago

0.12.2

2 years ago

0.14.1

2 years ago

0.13.2

2 years ago

0.14.2

2 years ago

0.13.3

2 years ago

0.13.4

2 years ago

0.10.0

2 years ago

0.3.0

2 years ago

0.9.0

2 years ago

0.7.2

2 years ago

0.8.0

2 years ago

0.7.1

2 years ago

0.9.2

2 years ago

0.9.1

2 years ago

0.5.0

2 years ago

0.4.0

2 years ago

0.3.1

2 years ago

0.7.0

2 years ago

0.6.0

2 years ago

0.5.1

2 years ago

0.2.2

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago

0.1.3

2 years ago

0.1.2

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago

0.0.1

2 years ago