S
SolidJS10mo ago
Basil

Using SolidJS's produce(), but with Immer-style patch generation?

Is it possible to use SolidJS's produce, but with patch generation akin to Immer's produceWithPatches? (being able to only store the delta (patches), as well as supporting things like classes) I'm currently using a combination of Immer's produceWithPatches and reconcile to work with a state that's made up of nested instances of various classes. The only reason I'm using Immer in the first place is to get the undo/redo functionality with little fuss, but it doesn't play well with SolidJS, and is slow and results in unnecessary re-renders.
6 Replies
deluksic
deluksic10mo ago
I believe you can just use the same function that you get in produce and run it through both immer and solid separately?
Brendonovich
Brendonovich10mo ago
i think solid primitives has something just for this, along with a separate undo/redo package https://primitives.solidjs.community/package/deep#capturestoreupdates
Solid Primitives
A library of high-quality primitives that extend SolidJS reactivity
Basil
BasilOP10mo ago
Immer has copy-on-write, so if I do something like this:
set(
solidProduce(
s => immerProduceWithPatches(s,
draft => {
draft.someParam = 7;
}
)
)
);
set(
solidProduce(
s => immerProduceWithPatches(s,
draft => {
draft.someParam = 7;
}
)
)
);
immerProduceWithPatches will generate patches, but it will not be changed on the underlying s object that Solid's produce (solidProduce here) gives you. Even applyPatches is copy-on-write, so it has the same issue. I'm messing around with that right now, but SolidJS stores in general don't seem to support reactivity with classes. Wrapping things in createMutable does help, but the granularity is lost; updating a value in a class causes captureStoreUpdates to return an instance of the class, instead of just the one property. Example:
import { createEffect } from 'solid-js';
import { captureStoreUpdates } from '@solid-primitives/deep';
import { createMutable } from 'solid-js/store';

class Subclass {
someVal: number;

constructor(i: number) {
this.someVal = i;

return createMutable(this);
}
}

class Root {
arr: Subclass[];
val: number;

constructor() {
this.arr = [
new Subclass(3),
new Subclass(8),
new Subclass(1),
new Subclass(4)
];

this.val = 9;

return createMutable(this);
}
}

export default function App() {
const store = new Root();

const getDelta = captureStoreUpdates(store);

createEffect(() => {
const delta = getDelta();
console.log('delta:', ...delta.map(a => a.path));
});

function storeUpdate() {
console.log('pre:', store.arr[1].someVal);

store.arr[1].someVal = 0;

console.log('post:', store.arr[1].someVal);
}

return (
<>
<button onClick={storeUpdate}>Update</button>
<p>{store.arr[1].someVal}</p>
<p>{store.val}</p>
</>
);
}
import { createEffect } from 'solid-js';
import { captureStoreUpdates } from '@solid-primitives/deep';
import { createMutable } from 'solid-js/store';

class Subclass {
someVal: number;

constructor(i: number) {
this.someVal = i;

return createMutable(this);
}
}

class Root {
arr: Subclass[];
val: number;

constructor() {
this.arr = [
new Subclass(3),
new Subclass(8),
new Subclass(1),
new Subclass(4)
];

this.val = 9;

return createMutable(this);
}
}

export default function App() {
const store = new Root();

const getDelta = captureStoreUpdates(store);

createEffect(() => {
const delta = getDelta();
console.log('delta:', ...delta.map(a => a.path));
});

function storeUpdate() {
console.log('pre:', store.arr[1].someVal);

store.arr[1].someVal = 0;

console.log('post:', store.arr[1].someVal);
}

return (
<>
<button onClick={storeUpdate}>Update</button>
<p>{store.arr[1].someVal}</p>
<p>{store.val}</p>
</>
);
}
When running store.arr[1].someVal = 0;, the path of the delta that gets generated is ['arr', 1], instead of the expected ['arr', 1, 'someVal']. It stores an instance of the Subclass class instead of just a number. Also, when you have nested createMutables, calling unwrap only unwraps the top-most value, and structuredClone still fails if any sub-properties aren't unwrapped. After re-reading this, I got the idea of re-implementing Immer's applyPatches, but in a mutative way, which solved my issue.
deluksic
deluksic10mo ago
Release as a package, I might need it soon lol
Basil
BasilOP10mo ago
Oh, it's certainly not production ready. It doesn't support Maps or Sets, since my project doesn't use those, but here you go:
/** Applies Immer patches mutatively. */
function applyPatches(patches: Patch[]) {
batch(() => {
for (const { path, op, value } of patches) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let base: any = get;
for (let i = 0; i < path.length - 1; i++) {
const frag = path[i];
base = base[frag];
}
const key = path[path.length - 1];

// Doesn't support `Map`s and `Set`s, but that doesn't matter, as we don't use them.
switch (op) {
case 'replace': {
// TODO: Handle `Map`s and `Set`s differently
// https://github.com/immerjs/immer/blob/8e465ab7f4d53525e72bc6b78e9204cb897cf528/src/plugins/patches.ts#L244
base[key] = value;
break;
}
case 'add': {
// TODO: Handle `Map`s and `Set`s differently
// https://github.com/immerjs/immer/blob/8e465ab7f4d53525e72bc6b78e9204cb897cf528/src/plugins/patches.ts#L262
if (Array.isArray(base)) {
key === '-'
? base.push(value)
: base.splice(key as number, 0, value);
} else base[key] = value;
break;
}
case 'remove': {
// TODO: Handle `Map`s and `Set`s differently
// https://github.com/immerjs/immer/blob/8e465ab7f4d53525e72bc6b78e9204cb897cf528/src/plugins/patches.ts#L273
if (Array.isArray(base)) base.splice(key as number, 1);
else delete base[key];
break;
}
}
}
});
}
/** Applies Immer patches mutatively. */
function applyPatches(patches: Patch[]) {
batch(() => {
for (const { path, op, value } of patches) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let base: any = get;
for (let i = 0; i < path.length - 1; i++) {
const frag = path[i];
base = base[frag];
}
const key = path[path.length - 1];

// Doesn't support `Map`s and `Set`s, but that doesn't matter, as we don't use them.
switch (op) {
case 'replace': {
// TODO: Handle `Map`s and `Set`s differently
// https://github.com/immerjs/immer/blob/8e465ab7f4d53525e72bc6b78e9204cb897cf528/src/plugins/patches.ts#L244
base[key] = value;
break;
}
case 'add': {
// TODO: Handle `Map`s and `Set`s differently
// https://github.com/immerjs/immer/blob/8e465ab7f4d53525e72bc6b78e9204cb897cf528/src/plugins/patches.ts#L262
if (Array.isArray(base)) {
key === '-'
? base.push(value)
: base.splice(key as number, 0, value);
} else base[key] = value;
break;
}
case 'remove': {
// TODO: Handle `Map`s and `Set`s differently
// https://github.com/immerjs/immer/blob/8e465ab7f4d53525e72bc6b78e9204cb897cf528/src/plugins/patches.ts#L273
if (Array.isArray(base)) base.splice(key as number, 1);
else delete base[key];
break;
}
}
}
});
}
Adel
Adel10mo ago
I ended up cleaning solid store's proxy before passing it to immer's produce/produceWithPatches:
/** removing solid store proxy's circular references before passing to immer */
const clean = <T>(t: T): T => structuredClone(unwrap(t))
/** removing solid store proxy's circular references before passing to immer */
const clean = <T>(t: T): T => structuredClone(unwrap(t))
Want results from more Discord servers?
Add your server