maverick-js / signals Goto Github PK
View Code? Open in Web Editor NEWA tiny (~1kB minzipped) and extremely fast library for creating reactive observables via functions.
License: MIT License
A tiny (~1kB minzipped) and extremely fast library for creating reactive observables via functions.
License: MIT License
In playing with Maverick Signals I came across a few neat features related to computed
signals that I didn't see covered by the current README. It would be great if these had a little more documentation, since they seem quite useful.
dirty
option to computed
scoped
option to computed
initial
argument to computed
and when it applies. I played with it briefly and seems that if you construct a computed with computed(f, { initial })
then the value of f()
is returned when peeking or reading the signal, and I wasn't sure when the initial value is used.I was also wondering about the nested computed behavior mentioned in this issue, which might deserve a little "note" section in the docs.
In particular, I was wondering if this is something to watch out for when building a DOM library on top of Signals where each component is a function and you can't be sure whether a computed
contains a nested call to computed
.
For example, if a component has a computed array of children, and those children are also components that have computed
values inside them, when will the nested computed
s be disposed? Or more generally – if we want to ensure that child computed
signals are disposed of at the appropriate time, should we always create computeds with { scoped: true }
just in case the computed function might transitively contain calls to computed
, and would not doing so potentially result in a memory leak?
for example this:
let $a = $observable(3)
$effect($on($a, (prev, value) => {
console.log(prev, value)
}))
// first, logs: undefined, 3
$a.set(5)
// now logs: 3, 5
I'm trying to implement maverick signals on https://pota.quack.uy/ . Solid and voby (oby) signals already work.
I'm having an issue with context, when using getContext
inside untrack
it will return undefined.
displays test
effect(()=>{
const id = Symbol()
setContext(id, "test")
console.log(getContext(id))
})
on the other hand, this will display undefined
effect(()=>{
const id = Symbol()
setContext(id, "test")
untrack(()=> console.log(getContext(id)))
})
This is the freshly installed clone, running pnpm test
.
❯ tests/observables.test.ts (0)
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL tests/observables.test.ts [ tests/observables.test.ts ]
SyntaxError: Unexpected token '??='
❯ new Script vm.js:102:7
❯ createScript vm.js:262:10
❯ Object.runInThisContext vm.js:310:10
❯ async /home/eguneys/js/observables/src/index.ts:1:256
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests no tests
Time 2.87s (in thread 0ms, Infinity%)
ELIFECYCLE Test failed. See above for more details.
Hi, is there a way to set a signal without triggering observables? At first I thought I could use untrack
but seems this is just for in an effect
?
import { root, signal, untrack, effect, tick } from "@maverick-js/signals";
root((dispose) => {
const $value = signal(0);
effect(() => {
console.log("$value changed to", $value());
});
// Still triggers the effect
untrack(() => {
$value.set(10);
});
tick()
dispose()
});
Hi, thanks for the lib I like the API a lot, but I noticed that effects weren't working in my code and so I copied your example from the README but the effect also isn't called there:
https://codesandbox.io/s/loving-ace-vz195z?file=/src/index.js:0-989
import { root, signal, computed, effect, tick } from "@maverick-js/signals";
root((dispose) => {
// Create - all types supported (string, array, object, etc.)
const $m = signal(1);
const $x = signal(1);
const $b = signal(0);
// Compute - only re-computed when `$m`, `$x`, or `$b` changes.
const $y = computed(() => $m() * $x() + $b());
// Effect - this will run whenever `$y` is updated.
const stop = effect(() => {
console.log("$y changed", $y());
// Called each time `effect` ends and when finally disposed.
return () => {};
});
$m.set(10); // logs `10` inside effect
// Flush queue synchronously so effect is run.
// Otherwise, effects will be batched and run on the microtask queue.
tick();
$b.set((prev) => prev + 5); // logs `15` inside effect
tick();
// Nothing has changed - no re-compute.
$y();
// Stop running effect.
stop();
// ...
// Dispose of all signals inside `root`.
dispose();
console.log("end");
});
I thought that "$y changed" would be called if $m, $x or $b change?
❯ node bench/layers.js
┌──────────┬───────┬───────┬────────┬─────────┬─────────┐
│ │ 10 │ 100 │ 500 │ 1000 │ 2000 │
├──────────┼───────┼───────┼────────┼─────────┼─────────┤
│ maverick │ 24.72 │ 47.93 │ 263.59 │ 593.43 │ 1486.25 │
├──────────┼───────┼───────┼────────┼─────────┼─────────┤
│ S │ 28.44 │ 63.92 │ 447.01 │ 1046.29 │ 2624.55 │
├──────────┼───────┼───────┼────────┼─────────┼─────────┤
│ solid │ 50.47 │ 92.48 │ 712.09 │ 1865.60 │ 8069.42 │
├──────────┼───────┼───────┼────────┼─────────┼─────────┤
│ solid2 │ 8.89 │ 94.50 │ 764.52 │ 1755.36 │ 7806.48 │
└──────────┴───────┴───────┴────────┴─────────┴─────────┘
Seems like V8 JIT affect time in small layer tier.
solid2 is just solid
report.solid = { fn: runSolid, runs: [] };
report.solid2 = { fn: runSolid, runs: [] };
Don't feel obligated to respond to this at all. 😄
But I had this thing based on another reactive library:
https://codesandbox.io/s/reactive-dom-lifecycle-zhzw7
A basic, working Sinuous clone using dipole
.
Well, your library looks awesome, so of course I had to try to port from dipole
to that:
https://codesandbox.io/s/reactive-dom-maverick-njvhsw
Essentially, there's no effects happening anywhere, and I have no clue why. 🤷♂️
As said, don't feel obligated to respond - this is most likely not due to any issue with your library, but just my lacking understanding of how it works. I would have posted this in Discussions if it were enabled. 😄
Not a major issue but i think Object.is
would be better for cheking signal updates:
Line 398 in 65f7fe9
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness
Can I create computeds inside other computeds? Are they going to be garbage collected?
Hi, I'm the author of S.js.. I learned about your framework and its benchmark when it was mentioned in a twitter conversation: https://twitter.com/RyanCarniato/status/1574305237933821953 .
I noticed that your benchmark for S.js has it doing 2x the work of the other implementations, as it's creating a second, unnecessary computation for each node. Specifically, this line is redundant and should be removed.
S(props.a), S(props.b), S(props.c), S(props.d);
props.a/b/c/d
are already computations. Wrapping them a second time with S(...)
makes a second, unnecessary computation out of those computations.
I don't know the history of this benchmark and gather that you may have copied it from somewhere else. If the upstream source has the same issue, would you mind pointing me towards it, so that I can get it fixed there too? Thanks!
Hi,
I really appreciate such a great library.
Is it possible to use and integrate Signals with React 18+ too?
If yes how? any example?
This line:
This fallback won't work - if queueMicrotask
not defined, it will fail.
You need something like window.queueMicrotask
, or more likely something like typeof queueMicrotask != 'undefined'
to remain compatible with Node.
Very nice implementation of observables! Very small and very readable codebase. 🙂🙏
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"test": "./dist/dev/index.js",
"development": "./dist/dev/index.js",
"default": "./dist/prod/index.js"
},
"./map": {
"types": "./dist/types/map.d.ts",
"test": "./dist/dev/map.js",
"development": "./dist/dev/map.js",
"default": "./dist/prod/map.js"
},
"./package.json": "./package.json"
}
But inside npm package, there is no dist directory.
import { signal, effect, tick } from '@maverick-js/signals'
let res = []
const A = signal(0)
const H = effect( ()=> res.push( A() ) )
A.set(1); tick()
$mol_assert_like( res, [ 1 ] )
Like this:
let $a = $observable([1,2,3])
let $b = $computed($mapArray($a, _ => { console.log(_); return { _ }))
// logs 1 2 3
$a.set([3,2,5])
// logs 5
Finally I want to know 1 is removed so some type of $onCleanup function inside $computed.
Also some type of notification to note that elements have reordered.
Hi, wondering if this is a known gotcha or otherwise I can try and make a repro:
Doesn't work (not called):
effect(() => {
this.viewportEl?.scrollTo(this.$scrollX(), this.$scrollY())
})
Does work:
effect(() => {
console.log(this.$scrollX(), this.$scrollY())
this.viewportEl?.scrollTo(this.$scrollX(), this.$scrollY())
})
Or:
effect(() => {
const scrollX = this.$scrollX()
const scrollY = this.$scrollY()
this.viewportEl?.scrollTo(scrollX, scrollY)
})
looks like it'd be part of the API, no?
Earlier I had an idea of using ownership tree for error handling too, was not sure if you would like it, but decided to show it anyway. I am using it in my reactive implementation, it works OK.
Basically we climb the ownership tree and see if parent scope has a handler. If you are observing strict ownership as Solid does, assigning owners to effects, memos and components, it should have the same effect as re-throwing and catching it somewhere higher on the component tree.
Here is how I implemented it for my own reactive system:
function handleError(scope: Computation, error: unknown) {
const handlers = scope.handlers;
let handled = false;
for (let i = 0, len = handlers.length; i < len; i++) {
try {
handlers[i](error);
handled = true;
break; // Error is handled.
} catch (rethrown: any) {
error = rethrown;
}
}
if (!handled) {
if (scope.owner) {
handleError(scope.owner, error);
} else {
throw error;
}
}
}
Now, parent scope will capture any error that may be thrown anywhere down under, including effects and memos. The logic is same as context lookup.
This may provide a simpler mental model for tracking the error path in the application.
Here is how error boundary would look:
export const ErrorBoundary: Component<{
children: JSX.Element,
fallback: JSX.Element | ((props: { error: any, reset: () => void }) => JSX.Element),
}> = ({ children, fallback }) => {
const noError = Symbol('no-error');
const error = signal<any>(noError);
onError((err) => error.set(err));
const reset = () => error.set(noError);
return memo(() => (error() !== noError) ?
typeof fallback === 'function' ? fallback({ error: error(), reset }) : fallback :
children
);
};
Line 268 in 65f7fe9
Once we had a discussion on keeping the language semantics when handling errors in SolidJS. Solid did not allow throwing falsy values as errors even though the language permits it. I see you follow somewhat similar path, but coercing errors may not be good idea from the consumer's perspective. Why not keep them as is:
let handled = false;
for (let i = 0, len = handlers.length; i < len; i++) {
try {
handlers[i](error);
handled = true;
break; // Error is handled.
} catch (rethrown: any) {
error = rethrown;
}
}
if (!handled) throw error;
Is there a valid reason for coercion?
SolidJS has stores for deeper objects: https://www.solidjs.com/docs/latest/api#using-stores
I only need 1 level like: https://github.com/nanostores/nanostores#maps
The use case is a model which has an object of data (e.g. User) and I want to observe each prop inside it, and also compute derived values if some of those props change.
I think signal
does a shallow check for changes and so what would you recommend to achieve 1 level deep observability?
const user = {
name: 'John',
age: 17,
}
$user = signal(user)
const $doubleAge = computed(() => $user().age * 2);
$doubleAge
should only re-compute if age
changes
I believe using an array for _sources
, _observers
, _children
, and _handlers
, and an object for _context
will reduce bundle size significantly, provides cleaner types and remove null checking conditionals that lie on the hot execution path since those values end up being an array, or object if it is context, almost always.
Now a simple push and pop will suffice for many of the actions. Setting a context value will be reduced to a simple assignment, no need to create new object when inserting a new value, and we can take advantage of recursion for scope related actions like disposal.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.