Gathering detailed insights and metrics for @signalis/core
Gathering detailed insights and metrics for @signalis/core
npm install @signalis/core
Typescript
Module System
Node Version
NPM Version
73.2
Supply Chain
98.4
Quality
75.7
Maintenance
100
Vulnerability
100
License
Total Downloads
4,617
Last Day
3
Last Week
10
Last Month
46
Last Year
295
Minified
Minified + Gzipped
Latest Version
0.1.0
Package Id
@signalis/core@0.1.0
Unpacked Size
56.98 kB
Size
15.45 kB
File Count
21
NPM Version
8.19.3
Node Version
16.19.0
Publised On
25 Apr 2023
Cumulative downloads
Total Downloads
Last day
-40%
3
Compared to previous day
Last week
-44.4%
10
Compared to previous week
Last month
119%
46
Compared to previous month
Last year
-92.2%
295
Compared to previous year
4
@signalis/core
signalis
is a lightweight library for reactivity influenced by @preact/signals
, solidjs
, and reactively
. It aims to expose a small set of highly composable, highly performant primitives for building reactive programs as simply as possible.
To that end, signalis
exposes three core primitives: signals, derived values, and effects.
Signals are the core unit of reactivity in signalis
(and, frankly, most reactivity systems!). A Signal
is simply a box around a value that tells other things when its value has changed. Whenever a Signal
changes, any part of the reactivity system that relies on it will update accordingly the next time that part of the system is accessed. In other words, all parts of the system will be kept in sync lazily: changing the value of a Signal
doesn't trigger any additional computations, it simply tells the parts of the system that care about the Signal
that they will need to recompute eventually.
To create a Signal
, use the createSignal
function:
createSignal<T>(value: T, isEqual?: false | (old: T, new: T) => boolean): Signal<T>
This is the form of createSignal
that you will likely use more often than not. It accepts a value and (optionally) an equality function. By default, Signal
will use ===
to determine when a particular Signal
's value has changed (and hence, when it needs to notify that it has updated), but you can provide your own equality function if you need to customize this behavior. Additonally, if you'd like to make the Signal
always notify when it's been, you can set the second argument to false
.
1// Basic usage 2const count = createSignal(0); 3const list = createSignal(['foo', 'bar', 'baz']); 4const me = createSignal({ firstName: 'Chris', lastName: 'Freeman' }); 5 6// to read a signal, access its `value` property 7count.value; // 0 8 9// to update a signal, assign a new value to its `value` property 10count.value = 1; 11 12// Since signals use `===` equality, object and array updates need to be immutable 13me.value = { 14 ...me.value, 15 firstName: 'Christopher', 16}; 17 18// Pass a custom equality function to change how signals determine when they've changed 19import { isEqual } from 'lodash/isEqual'; 20const meButMutable = createSignal( 21 { 22 firstName, 'Chris', 23 lastName: 'Freeman' 24 }, 25 isEqual 26)
createSignal(): Signal<unknown>
In some cases, you don't actually care about the value of a signal, but instead simply need a way to tell other parts of the system about a change. In those cases, this form of createSignal
can make that process easier. Calling createSignal
with no arguments will return a Signal<unknown>
with its equality function set to false
so that it will notify its dependencies every time value
is set to a value.
1const notifier = createSignal(); 2 3notifier.value = null; // will notify all dependents 4 5notifier.value = null; // will notify again
As you may have already realized, a Signal
on its own isn't very useful without something that reacts to it. This is where Derived
comes in. Derived
is a readonly reactive value that, as its name might suggest, is derived from one or more other reactive values (which can be Signal
s, other Derived
, or a combination of both). The value of a Derived
will update any time one of its dependents change, and it will also notify its own dependents whenever it changes.
To create a Derived
, use createDerived
:
createDerived<T>(fn: () => T): T
createDerived
accepts a callback that reads one or more reactive values, does some computation with them, and returns the result. createDerived
should always be used to represent a reactive value, so it's important that the callback actually return something. Furthermore, the callback passed into createDerived
should be a pure function (it shouldn't have any side effects). If you need to represent a reactive function, there's a different primitive for that (see createEffect
below).
1const firstName = createSignal('Chris'); 2const lastName = createSignal('Freeman'); 3 4const fullName = createDerived(() => `${firstName.value} ${lastName.value}`); 5 6// Since Derived is a reactive value, you access its value the same way you would a Signal 7fullName.value; // 'Chris Freeman' 8 9// Derived can rely on other Derived 10const fullNameBUTYELLING = createDerived(() => `${fullName.value.toUpperCase()}!!!!!!!!!!!!!`); 11 12fullNameBUTYELLING.value; // 'CHRIS FREEMAN!!!!!!!!!!!!!' 13 14// Setting the value of a Signal at the root of a chain of Derived will notify all of them that they may need to recompute 15firstName.value = 'Christopher'; 16 17fullNameBUTYELLING.value; // 'CHRISTOPHER FREEMAN!!!!!!!!!!!!!'
In order to avoid doing any unnecessary work, Derived
is very clever about when it should actually recompute. It will never recompute just because one of its sources changed. Something else has to first try and read its value
property before it will even consider recomputing. Once something does access its value
, Derived
follows the heuristic below to determine if it should actually recompute:
Derived
has also changed, so recompute immediately.Here's an example to help clarify further:
1const count = createSignal(1); 2 3// This will recompute whenever count changes 4const countIsOdd = createDerived(() => count.value % 2 !=== 0); 5 6// This will recompute whenever countIsOdd changes 7const oddOrEven = createDerived(() => { 8 if (countIsOdd.value) { 9 return 'odd'; 10 } else { 11 return 'even'; 12 } 13}) 14 15 16// Trigger the `countIsOdd` computation since it has never been read before. 17countIsOdd.value // true, since count.value === 1 18 19// Trigger the `oddOrEven` computation since it *also* has never been run before. However, since `countIsOdd` has already been read, and `count` hasn't changed, `countIsOdd` does *not* recompute. 20oddOrEven.value // 'odd', since countIsOdd.value === true 21 22 23count.value = 3; // Set count to another odd number 24 25/** 26 * Read `oddOrEven` again. At this point, `countIsOdd` has not recomputed since nothing else has accessed its `value` property since we updated `count`. 27 * 28 * `oddOrEven` knows that it *might* need to recompute since `count` changed, but it's not certain since it doesn't depend directly on `count`, so we first 29 * check to see `countIsOdd` should recompute. 30 * 31 * `countIsOdd` sees that its direct dependency (`count`) has changed, so it recomputes and but since its value is still `true`, it tells `oddOrEven` that not to recompute. 32 * 33 * `oddOrEven`, upon being told that its direct dependencies haven't changed, returns its previous value ('odd') and skips computing 34 * 35 */ 36oddOrEven.value // still 'odd', no need to recompute since we know `countIsOdd` hasn't changed
While Derived
is a reactive value, you can think of an effect as a reactive function. It reacts to changes the same way that Derived
does, but rather than returning a value, it runs a computation. Unlike Derived
, side effects are welcome (and, in fact, encouraged) in effects. Effects
use the same heuristic as Derived
for determining when to recompute, with the one exception that they evaluate their callback function eagerly, whereas Derived
is lazy. (This makes sense when you consider that effects don't have values, and therefore there's no way to "read" it the way you would with a Derived
).
Effects are useful any time you need to perform some kind of action in response to something else changing.
To create an effect, use createEffect
:
createEffect(fn: () => void | (() => void)): () => void
createEffect
takes a callback that reads some number of reactive values and does something with them. The callback should not return a value (it can return a value, but signalis
won't do anything with it since effects aren't mean to represent values). createEffect
returns a disposal function that can be called if/when you no longer need the effect anymore
By default, the disposal function will simply disable the effect and remove it from the reactivity tree, but you can also add additional cleanup behavior by return a function from the callback that contains whatever action you'd like to run when the effect is disposed.
Effects have a wide range of uses, let's take a look at a few examples.
In the most basic case, we simply set up an effect that runs every time a signal changes:
1const count = createSignal(0); 2 3createEffect(() => { 4 console.log(`The value of count is: ${count.value}`); 5}); 6 7// since effects are eager, we will immediately log 8 9count.value = 1; // the effect logs 'The value of count is 1'
Effects can also respond to Derived
, and will use the same logic as Derived
when determining whether to recompute:
1const count = createSignal(1); 2const countIsOdd = createDerived(() => count.value % 2 !=== 0); 3 4let message: string; 5 6createEffect(() => { 7 message = countIsOdd.value ? 'odd' : 'even'; 8}); 9 10console.log(message); // 'odd' 11 12count.value = 2; 13 14// effect recomputes since we've gone from odd to even 15console.log(message); // 'even' 16 17count.value = 4; 18 19// effect does *not* recompute since we've gone from one even number to another 20console.log(message) // 'even'
Since effects compute eagerly, it's important that we provide a way to clean them up in the event that we no longer need them. To do that, we can use the function createEffect
returns:
1const count = createSignal(0); 2 3const dispose = createEffect(() => { 4 console.log(`count.value is: ${count.value}`); 5 6 return () => { 7 console.log(`cleaning up! no more messages!`); 8 }; 9}); // logs the count immediately 10 11count.value = 1; // logs again 12 13dispose(); // logs the clean up message 14 15count.value = 2; // no log, since the effect is disposed now
Finally, we can return a function from the callback to customize our effect's cleanup behavior:
1function createTimer() { 2 const time = createSignal(0); 3 4 let interval; 5 6 const dispose = createEffect(() => { 7 console.log(time.value); 8 9 return () => { 10 if (interval) { 11 clearInterval(interval); 12 } 13 console.log('Stopped!'); 14 }; 15 }); 16 17 return { 18 start() { 19 interval = setInterval(() => { 20 time.value++; 21 }); 22 }, 23 stop() { 24 dispose(); 25 }, 26 }; 27} 28 29const timer = createTimer(); 30 31timer.start(); // effect starts logging every second, 1...2...3...4...etc. 32 33timer.stop(); // interval gets cleared, the effect is cleaned up, and it logs 'Stopped!'
A Resource
is a reactive abstraction built to help developers incorporate asynchronous values into reactive systems. Signalis' Resource
is heavily inspired by resources in SolidJS
, though its API is quite a bit different.
Resource
comes in two flavors:
A Resource
exposes four pieces of state:
value
: Signal<ValueType | undefined>
- The value of the most recent async request (this is the most up to date value)last
: ValueType | undefined
- The value of the previous run of the async requestloading
: Signal<boolean>
- Whether or not the Resource
is currently in the process of fetchingerror
: Signal<unknown>
- A Signal
whose value will be populated with the contents of an error that is caught during the fetcher's execution.createResource<ValueType>(fetcher: () => Promise<ValueType>): Resource<ValueType>
The single argument version of createResource
accepts a function that performs some kind of async operation and returns a Promise
. When the Resource
is created, it will invoke the fetcher
function and then updates the Resource
's value
property once the async request is complete. Resource
also has a refetch
method that will re-run the fetcher
function and trigger updates to the Resource
's reactive properties accordingly
1const postResource = createResource(() => fetch('myUrl.com').then((res) => res.json())); 2 3let error = ''; 4let content = ''; 5 6if (postResource.loading.value) { 7 content = 'loading'; 8} else if (postResource.error.value) { 9 error = postResource.error.value; 10} else { 11 content = postResource.value; 12} 13 14// run the fetch request again 15postResource.refetch();
createResource<SourceType, ValueType>(source: Signal<SourceType> | Derived<SourceType>, fetcher: (source: SourceType) => Promise<ValueType>): ResourceWithSource<ValueType, SourceType>
The two-argument version of createResource
allows you to designate a reactive value as a "source" for the fetcher function, and returns a ResourceWithSource
. Rather than calling the fetcher
immediately, a ResourceWithSource
will fire the fetcher as long as the source value is not null
, undefined
, or false
. If the source is truthy to begin with, the fetcher function will be fired as soon as it's create. ResourceWithSource
will also re-fire the fetcher function any time the source value changes, updating its value
property with the result and notifying any dependents of the change. The fetcher function passed to ResourceWithSource
will receive the source value as an argument.
1const pageNumber = createSignal(false); 2const postResource = createResource( 3 pageNumber, 4 (pageNumber: number) => 5 fetch(`api.com/posts/${pageNumber}`).then((res) => res.json()) 6); 7 8// No HTTP request has happened yet since `pageNumber` is false. 9 10pageNumber.value = 1; 11 12// Now that pageNumber has been updated, `postResource` will make the HTTP request using the pageNumber and update its 13// `value` property, notifying anything that depends on it. 14 15let error = ''; 16let content = ''; 17 18if (postResource.loading.value) { 19 content = 'loading'; 20} else if (postResource.error.value) { 21 error = postResource.error.value; 22} else { 23 content = postResource.value; 24} 25 26// run the fetch request again 27postResource.refetch();
While the primitives covered above can technically cover all of the use cases for reactivity that you might encounter, having to call .value
on every single read or write of every Signal
or Derived
can become a bit cumbersome, especially if you're trying to model more complicated states. In cases where you need highly nested reactivity, you'll likely that find that a Store
is a more ergonomic solution.
Stores are objects (or arrays!) that have been wrapped in a Proxy that lazily wraps the object's properties in Signals as they are accessed. This gives you more fine-grained reactivity than you would get by simply wrapping an entire object in a Signal, while also making it easier to work with by calling .value
on each read and write behind the scenes so you don't have to.
createStore<T extends object>(v: T | Store<T>): Store<T>
To create a Store, simply pass an object or array to createStore
. Once you have a Store, any time you access one of its properties, the store will make it reactive and give you the value. Note that this only works for JS primitives (e.g. strings, numbers, booleans, etc.) and regular objects and arrays.
createStore
also handles this
binding for you, which means that you can define getters and methods on the object being passed into the store and they'll work like you'd expect (see the example below for an example of what this looks like).
update<T extends object>(base: T, recipe: (draft: T) => void): T
While property access on stores
behaves just like it would with a normal POJO or array, the process of writing to the store is a bit different. Stores can't be mutated
directly, and will throw an error if you try. This is because Signalis needs to do some careful bookkeeping behind the scenes to make sure that store changes correctly trigger updates to all the other reactive entities that observe it.
To update a store, Signalis provides an update
function that behaves similarly to both Immer's produce
and Solid's produce
(Note: you will find that stores in general are similar to Solid's stores. This is because our stores are heavily inspired by the great work Solid has already done!). update
takes two arguments: the store you want to update, and a callback that receives a draft version of the store and executes all of the changes you want to make. Unlike produce
, however, update
mutates the store for you, you don't need to actually do anything the result of calling update
. All of the changes you make to the draft in the callback will be reflected in the store when update
is finished running. That said, update
still does return the updated store itself in case you would like to use the result yourself.
For example, let's say we wanted to model a classic todo list. Here's what it would look with raw Signals vs. a Store:
1// with signals
2const todos = createSignal([]);
3const todoCount = createDerived(() => todos.length);
4const todoStore = createSignal({
5 todos,
6 todoCount,
7});
8
9// add a todo
10todoStore.todos.value = [
11 ...todoStore.todos.value,
12 createSignal({ id: '1', text: 'Use a store for this' }),
13];
14
15// get all todo names
16todoStore.todos.value.map((todo) => todo.value.text);
17
18// with a Store
19const todoStore = createStore({
20 todos: [],
21
22 // we can define getters and method right on the store and `createStore` which will make sure
23 // they're bound correctly
24 get todoCount() {
25 return this.todos.length;
26 },
27
28 addTodo(newTodo) {
29 // Since every reactive property in a store is also a Store, you can pick properties off the
30 // root store and pass them to `update` just like you would any other Store.
31 update(this.todos, (draft) => {
32 draft.push(newTodo);
33 });
34 },
35
36 listTodoNames() {
37 this.todos.map((todo) => todo.text);
38 },
39});
isSignal(v: any): v is Signal<unknown>
Indicates whether or not the value passed in is a Signal
.
isDerived(v: any): v is Derived<unknown>
Indicates whether or not the value passed in is a Derived
.
No vulnerabilities found.
No security vulnerabilities found.