luisherranz / deepsignal Goto Github PK
View Code? Open in Web Editor NEWDeepSignal 🧶 - Preact signals, but using regular JavaScript objects
License: MIT License
DeepSignal 🧶 - Preact signals, but using regular JavaScript objects
License: MIT License
Hi,
I am new to deepsignals.
I face this issue when using effect as the example in Readme.md:
effect(() => {
console.log(state.counter);
});
Could you pls help to let me know where effect should i import from? Do i need to use effect from Signals along with Deepsignals?
Currently shouldProxy
omits functions and builtins from proxying https://github.com/luisherranz/deepsignal/blob/main/packages/deepsignal/core/src/index.ts#L137-L144, however in the project I'm working on currently we have other classes that are being stored in a deepsignal that we need to omit from proxying (in this case because they are already proxies and double-proxying causes issues).
I was able to work around the problem for now by adding the constructor to globalThis
so that deepsignal thinks it's a builtin, but this isn't ideal. It seems quite likely to me that there will be other use cases that will have classes that want to be stored in deepsignal but not have them proxied from one reason or another.
I can see a few ways this might be solved:
import { deepSignal, omit } from 'deepsignal';
deepSignal({ a: { b: 1 }, c: omit({ d: 2 }) })
import { deepSignal, omit } from 'deepsignal';
deepSignal({ a: { b: 1 }, c: { d: 1 } }, { omit: ['c'] })
import { deepSignal, omit } from 'deepsignal';
omit(SomeConstructor);
deepSignal({ a: { b: 1}, c: new SomeConstructor({ d: 1 }) });
I think this functionality could be added without adding too much to size bundle. If you're open to this functionality being added but don't have time to add it, let me know what your preferred path would be and I'd be happy to send you a PR.
Object.defineProperty
is a commonly used approach to define new object properties - internally, Reflect.set(...)
will invoke Object.defineProperty
anyway whenever a new property is to be set.
For that reason, it is a good idea to implement the "defineProperty" trap in order to catch all situations, where new properties are defined.
The required changes should be trivial (see my own fork of your repository)
$
is a common name for object properties, particularly in browser environments (the best-known example should be jQuery)
While it is a nice idea to access object property signals using $name
or array element signals using $[index]
, it should remain possible to use $
by itself as the name of an object (not an array) property - and $$
as the name for its related signal.
The required changes should be trivial (see my own fork of your repository)
First of all: thank you very much for your marvellous work!
It looks brilliant - but lacks a feature I currently need: I want to get informed whenever a new property is added to an observed object.
After looking into your code, I quickly hacked the required changes in a way I can live with - but these changes are neither properly tested nor completely free of side effects (I'm currently using a "helper property" with a literal key rather than a JavaScript symbol - I did that in order not to touch too many parts of your code)
If you are interested, just have a look into my fork - perhaps, you'll come up with a better (or more professional) solution?
In my code, I need to get the "raw" array from my deepsignal state, and I thought the RevertDeepSignal
type might help me get there, but while I can get a clean array back - all of the 20,000 items in that array are Proxy objects. When I pass the array to a function that has no concept of signals, it treats the array as if it were empty.
Please take a look at the demonstrate()
function below to see how I was hoping to get the raw array value back.
Note: in my particular case, I don't need the items in the arrays to be "deepsignaled", and those item's properties, and those item's properties... Just the array itself.
I'm not sure if that is an option?
interface WorkbookState {
loadingState: WorkbookLoadingState;
attributes: Attribute[];
categories: Category[];
hierarchies: Hierarchy[];
hierarchy: Hierarchy | null;
hierarchyNodes: HierarchyNode[];
lrsSegments: Segment[];
segments: Segment[];
trees: ExplicitDataSource<PlanningTreeData> | null;
workbook: Workbook | null;
}
const initialState: WorkbookState = {
attributes: [],
categories: [],
hierarchy: null,
hierarchyNodes: [],
hierarchies: [],
loadingState: WorkbookLoadingState.Initial,
lrsSegments: [],
segments: [],
trees: null,
workbook: null,
};
const store: WorkbookStore = {
state: deepSignal<WorkbookState>(initialState),
closeWorkbook,
openWorkbook,
setHierarchy,
};
const demonstrate = () => {
const attributes: Attribute[] = Object.values<Attribute>(
store.state.attributes as RevertDeepSignal<typeof store.state.attributes>,
);
// while the attributes array is properly types, attributes[n] are Proxy objects.
// when I pass them to my AttributeLookup class, they do not work because the elements are Proxy objects
const attributeLookup = new AttributeLookup(attributes);
// my current work around is to do a deep copy of property to eliminate proxies
const attributes: Attribute[] = toRaw(store.state.attributes);
}
function toRaw(val: any): any {
if (val instanceof Array) return val.map((val) => toRaw(val));
if (val instanceof Object)
return Object.fromEntries(
Object.entries(Object.assign({}, val)).map(([k, v]) => [k, toRaw(v)]),
);
return val;
}
Hi @luisherranz
I'd like to start off by saying I think the API you've written here is absolutely excellent. I've rewritten around 1000 lines of old contexts to deep signals, and everything is way easier to reason with.
I have run into one issue so far, and this might be entirely down to user error. I've narrowed things down to the minimal reproduction below
import { deepSignal } from "deepsignal";
type Example = {
testKey: Record<string, string | boolean>;
};
const initialState: Example = {
testKey: { testValue: false },
};
const state = deepSignal<Example>(initialState);
const testKey: Example["testKey"] = { testValue: true };
state.testKey = testKey;
I get the following error:
Type 'Record<string, string | boolean>' is not assignable to type 'DeepSignalObject<Record<string, string | boolean>>'.
Type 'Record<string, string | boolean>' is not assignable to type '{ [x: `$${string}`]: Signal<string | boolean> | undefined; }'.
'string' and '`$${string}`' index signatures are incompatible.
Type 'string | boolean' is not assignable to type 'Signal<string | boolean> | undefined'.
Type 'string' is not assignable to type 'Signal<string | boolean>'.ts(2322)
I see a similar error with
type Example = {
testKey: { [key: string]: string | boolean };
};
however if I change the string into a literal,
type Example = {
testKey: Record<"someLiteral", string | boolean>;
};
or
type Example = {
testKey: { ["someLiteral"]: string | boolean};
};
I no longer get the error. I have tried various permutations of using optional values / undefined unions, but to no real avail.
Do you see anything here that I'm doing incorrectly here?
Thanks very much!
Hi there ! Thank you for this lib, it makes a perfect combo with preact/signals.
When I use the counter example in React from the docs :
import { deepSignal } from "deepsignal";
const state = deepSignal({
count: 0,
get double() {
return state.count * 2;
},
});
function Counter() {
return (
<button onClick={() => (state.count += 1)}>
{state.$count} x 2 = {state.$double}
</button>
);
}
the component is re-rendered when I use {state.$double}
.
The same computed value doesn't make a re-render when I use it with preact directly: const double = computed(() => count.value*2)
.
If I use the same principe with deepsignal, it doesn't make a re-render :
get double() {
return computed(() => state.count * 2);
},
In the docs, you wrote : deepsignal will convert getters to computed values instead of signal instances underneath.
So I am not sure what's happening here. Is it the behaviour we should expect when using the signal state.$double
?
Thanks a lot for your help !
Hi luisherranz
First of all thank u for the nice library.
Is the smoothest state management dx in react imo. Dangerously smooth!
I came across a bit of an unpleasantry in dx with what looks like stale state, caused by unwanted nesting of the proxy-state.
Say I have the following store
const store = deepsignal({ renderedNode: undefined, nodes: [{id: 1, x: 0, y: 0}, ...])
if I would do
store.renderedNode = store.nodes[0];
store.renderedNode would become a nested proxy.
accessing and mutating this proxy would cause inconsistent state, I assume because the outer-proxy consumes all the mutations.
It is possible to prevent this bug by doing
store.$renderedNode.value = store.nodes[0];
But I wonder if it would be possible to prevent this overwrapping/unwanted nesting automatically, p.ex by checking for a symbol on the proxy.
I think solidjs does something similar with their createStore.
Lit now supports Preact Signals, so it'd be great to share an example in the readme about how to use it with deepsignal
.
References:
Hey @luisherranz! I've been working on incorporating @wordpress/
package updates into Calypso. The published packages has a peer dependency violation:
deepsignal@npm:1.3.4 [411e6] doesnt provide react (p9b425), requested by @preact/signals-react
yarn explain peer-requirements p9b425
➤ YN0000: deepsignal@npm:1.3.4 [411e6] doesnt provide react, breaking the following requirements:
➤ YN0000: @preact/signals-react@npm:1.3.4 [ed871] → ^16.14.0 || 17.x || 18.x ✘
➤ YN0000: use-sync-external-store@npm:1.2.0 [23689] → ^16.8.0 || ^17.0.0 || ^18.0.0 ✘
➤ YN0000: Note: these requirements start with @preact/signals-react@npm:1.3.4 [ed871]
While I've just ignored the error on our side, I think this warning will show up in most dependency tools when installing deepsignal.
Sorry for bothering you, but I must be completely blind right now...
Consider the following code
import { deepSignal } from 'https://unpkg.com/[email protected]/dist/deepsignal.module.js'
let Test = deepSignal({
_x:0,
get x () { return this._x },
set x (newX) {
if (newX !== this.x) { this._x = newX }
}
})
console.log('Test.x is defined as',Object.getOwnPropertyDescriptor(Test,'x'))
console.log('\ntrying to set "Test.x" to 1\n')
Test.x = 1
This code crashes with:
Test.x is defined as {enumerable: true, configurable: true, get: ƒ, set: ƒ}
configurable: true
enumerable: true
get: ƒ x()
set: ƒ x(newX)
[[Prototype]]: Object
trying to set "Test.x" to 1
index.ts:103 Uncaught TypeError: Cannot set property value of [object Object] which has only a getter
at Object.set (index.ts:103:30)
at VM6069 about:srcdoc:28:10
Do you have any idea why my code fails? I had the impression, that deepSignal
would support getters and setters...
I am using @preact/signals-react with react-native and it works but in many places I have a deeply nested object, to which I need to listen to property changes and re-render accordingly.
DeepSignals with @preact/signals-react would be the ideal solution.
When attempting to write a getter which consolidates a nested object, signals throws an error.
Reproduction:
const state = deepSignal({
x: {
a: 1,
b: 2,
},
get y() {
return Object.values((state.x as RevertDeepSignal<typeof state.x>) ?? {});
},
});
console.log(state.y);
Error:
Error: Computed cannot have side-effects
at mutationDetected (node_modules/.pnpm/@[email protected]/node_modules/@preact/signals-core/src/index.ts:5:8)
at value (node_modules/.pnpm/@[email protected]/node_modules/@preact/signals-core/src/index.ts:314:4)
at Object.values [as ownKeys] (node_modules/.pnpm/[email protected][email protected]/node_modules/deepsignal/core/src/index.ts:120:29)
at Function.values (<anonymous>)
It took me quite a while to figure out, but when trying the following code
let hugo = deepSignal({ })
let anna = hugo.anna = {
_list:[1,2,3],
get list () { return this._list.slice() }
}
effect(() => { console.log('> anna.list',anna.list) })
console.log('assign')
anna._list = anna._list.slice(0,2).concat(2.5,anna._list.slice(2))
anna._list = anna._list.slice(0,2).concat(anna._list.slice(3))
you will see that changes to anna.list
will not be recognized.
There is, however, a simple workaround: just observe anna
itself
let anna = hugo.anna = deepSignal({
_list:[1,2,3],
get list () { return this._list.slice() }
})
and everything works as intended
I'm attempting to create a store using deepsignal
in TypeScript.
I'm having a problem when I attempt to type my store, and in particular my Store's state object. Don't I need to declare state as a type of DeepSignalObject? But that type is not exported from the library.
interface AuthState {
auth: boolean;
}
interface AuthActions {
isAuthenticated: () => boolean;
setIsAuthenticated: (isAuthenticated: boolean) => void;
signOut: () => void;
}
interface DeepAuthState {
state: DeepSignalObject<AuthState>;
}
type AuthStore = DeepAuthState & AuthActions;
export const authStore: AuthStore = {
state: deepSignal<AuthState>({
auth: false,
}),
isAuthenticated() {
return this.state.auth;
},
setIsAuthenticated(isAuthenticated: boolean): void {
this.state.auth.value = isAuthenticated;
},
signOut(): void {
setIsAuthenticated(false);
},
};
Cannot proxy a Map
.
I get,
Uncaught Error: This object can't be observed.
I like the DX of using the signal.value
by default and accessing the underlying signal with the $
prefix, but context switching when using computed values is awkward. It would be nice to provide computed
with the same functionality as Preact's computed
, only returning a proxy object instead for the improved DX
import * as React from "react";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import { useSignal } from "@preact/signals-react";
import { useDeepSignal } from "deepsignal/react";
export default function BasicTextFields() {
const state = useDeepSignal({
textField1: "",
textField2: "",
textField3: ""
});
const onHandleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
state[name] = value;
};
return (
<Box
component="form"
sx={{
"& > :not(style)": { m: 1, width: "25ch" }
}}
noValidate
autoComplete="off"
>
<TextField
name="textField1"
id="outlined-basic"
label="Outlined"
variant="outlined"
onChange={onHandleChange}
value={state.$textField1.value}
/>
<TextField
name="textField2"
id="filled-basic"
label="Filled"
variant="filled"
onChange={onHandleChange}
value={state.$textField2.value}
/>
<TextField
name="textField3"
id="standard-basic"
label="Standard"
variant="standard"
onChange={onHandleChange}
value={state.$textField3.value}
/>
</Box>
);
}
Why am I not able to update the textField values? I want to have a common changeHandler. Thanks.
This is my stackblitz: https://codesandbox.io/s/affectionate-worker-tkgfqv?file=/demo.tsx:0-1303
example:
const sign = deepSignal<{ key?: string }>({ });
setTimeout(() => {
sign.key = 'value';
}, 1000);
setTimeout(() => {
delete sign.key;
}, 2000);
effect(() => {
console.log('sign', sign.key);
});
expected behavior:
undefined
value
undefined
current behavior:
undefined
value
see for an example with solid's createMutable
Thanks for this library, does it possible to use solidjs signal and usignal? i think they share common API
Consider the following code:
import { deepSignal } from 'https://unpkg.com/[email protected]/dist/deepsignal.module.js'
class TestClass {
_Prop = 0
get Prop () { return this._Prop }
set Prop (newValue) {
this._Prop = newValue
this.logIt()
}
logIt () {
console.log('internal report: Prop was just set to',this.Prop)
}
}
let Test = deepSignal(new TestClass())
/**** now run some tests ****/
console.log('observe "Test.Prop"')
effect(() => {
console.log('>>>> external effect: current "Prop" is now',Test.Prop)
})
console.log('\nsetting Test.Prop to 1\n\n')
Test.Prop = 1
console.log('\nsetting Test.Prop to 2\n\n')
Test.Prop = 2
(mind the import statement which imports your published module without any of my modifications!)
This code produces the following output:
observe "Test.Prop"
>>>> external effect: current "Prop" is now 0
setting Test.Prop to 1
>>>> external effect: current "Prop" is now 0
internal report: Prop was just set to 0
>>>> external effect: current "Prop" is now 1
setting Test.Prop to 2
>>>> external effect: current "Prop" is now 1
internal report: Prop was just set to 1
>>>> external effect: current "Prop" is now 2
I don't really worry about the useless effect callback invocations for already set values (although they are a bit ugly), but I worry about getter invocations following a set operation which still deliver an old value!
Do you have any idea?
Hi,
Thanks for the excellent library.
I think I'm missing something fundamental but is it possible to define an effect that will update after any change to an array of objects, and not just a specific property of one of the objects?
Here's an example of what I mean:
const state = deepSignal({
myArray: [
{
count: 0,
},
{
count: 0,
},
],
});
const IncrementArray = () => {
return (
<Button onClick={() => state.myArray[0].count++}>Increment</Button>
);
};
// Updates after increment
effect(() => {
if (state.myArray[0].count) {
console.log("state.myArray[0].count has changed");
}
});
// Doesn't update after increment
effect(() => {
if (state.myArray[0]) {
console.log("state.myArray[0] has changed");
}
});
// Also doesn't update after increment
effect(() => {
if (state.myArray) {
console.log("state.myArray has changed");
}
});
Is there any way to write either of those last two effects so that they will trigger when any property changes for any element in the array?
I know I can get the second effect working by incrementing the count like this instead:
<Button
onClick={() =>
(stateTest.myArray[0] = {
...stateTest.myArray[0],
count: stateTest.myArray[0].count + 1,
})
}
>
Increment
</Button>
But I am really loving the ability to modify the property directly that this library provides.
Thanks!
Consider the following code:
import { effect } from '@preact/signals-core'
import { deepSignal } from 'deepsignal'
let hugo = deepSignal({
list:[1,2,3]
})
effect(() => { console.log('> hugo.list',JSON.stringify(hugo.list)) })
console.log('push'); hugo.list.push(4) // works as intended
console.log('pop'); hugo.list.pop() // -> [1,2,3,null] -> [1,2,3]
console.log('shift'); hugo.list.shift() // [2,2,3] > [2,3,3] > [2,3,null] > [2,3]
console.log('unshift'); hugo.list.unshift(1) // [2,3,3] > [2,2,3] > [1,2,3]
console.log('insert'); hugo.list.splice(2,0,2.5) // [1,2,3,3] > [1,2,2.5,3]
console.log('delete'); hugo.list.splice(2,1) // [1,2,3,3] > [1,2,3,null] > [1,2,3]
console.log('assign')
hugo.list = hugo.list.slice(0,2).concat(2.5,hugo.list.slice(2)) // reacts 2x
This code produces the following output:
> hugo.list [1,2,3]
push
> hugo.list [1,2,3,4]
pop
> hugo.list [1,2,3,null]
> hugo.list [1,2,3]
shift
> hugo.list [2,2,3]
> hugo.list [2,3,3]
> hugo.list [2,3,null]
> hugo.list [2,3]
unshift
> hugo.list [2,3,3]
> hugo.list [2,2,3]
> hugo.list [1,2,3]
insert
> hugo.list [1,2,3,3]
> hugo.list [1,2,2.5,3]
delete
> hugo.list [1,2,3,3]
> hugo.list [1,2,3,null]
> hugo.list [1,2,3]
assign
> hugo.list [1,2,2.5,3]
> hugo.list [1,2,2.5,3]
As you can see, the effect
callback is invoked far too often - and the intermediate calls often log wrong lists (at least "wrong" from a functional point of view, what we see here might reveal some unwanted implementation details)
I didn't see in the unit tests or the documentation an example on how one might set the entire state object.
I assume if I can extract the values using RevertDeepSignal
, there might be a mechanism to set the values?
Take the following example. I assume that closeWorkbook2()
will work as expected, but is there a simpler approach that works more like closeWorkbook1
?
enum WorkbookLoadingState {
Initial,
Loading,
Success,
Error,
}
interface WorkbookState {
loadingState: WorkbookLoadingState;
attributes: Attribute[];
categories: Category[];
hierarchies: Hierarchy[];
hierarchy: Hierarchy | null;
hierarchyNodes: HierarchyNode[];
lrsSegments: Segment[];
segments: Segment[];
trees: ExplicitDataSource<PlanningTreeData> | null;
workbook: Workbook | null;
}
interface WorkbookActions {
loadWorkbook: (workbookId: string) => void;
closeWorkbook: () => void;
setHierarchy: (hierarchy: Hierarchy) => void;
}
type WorkbookStore = WorkbookActions & {
state: DeepSignal<WorkbookState>;
};
const initialState:WorkbookState = {
attributes: [],
categories: [],
hierarchy: null,
hierarchyNodes: [],
hierarchies: [],
loadingState: WorkbookLoadingState.Initial,
lrsSegments: [],
segments: [],
trees: null,
workbook: null,
};
// Not typesafe
const closeWorkbook1 = () => {
store.state = initialState;
}
const closeWorkbook2 = () => {
batch(() => {
store.state.attributes = [];
store.state.categories = [];
store.state.hierarchy = null;
store.state.hierarchyNodes = [];
store.state.hierarchies = [];
store.state.loadingState = WorkbookLoadingState.Initial;
store.state.lrsSegments = [];
store.state.segments = [];
store.state.trees = null;
store.state.workbook = null;
});
}
const store: WorkbookStore = {
state: deepSignal<WorkbookState>(initialState),
closeWorkbook1,
};
Preact was just upgraded fixing a bunch of issues we were having, upgrading @preact/signals-react
seems to break deepsignal
proxy observers and mutations on proxy don't trigger react render.
I don't have specific logs, there aren't any to show, it is simply not working with the latest upgrade, understandably.
Do you have plans to upgrade to the latest @preact/signals-react
usage? If so, let us know what the timeframe might be.
Thanks
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.