Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stores working with immutable data #3584

Closed
daliusd opened this issue Sep 18, 2019 · 11 comments
Closed

Stores working with immutable data #3584

daliusd opened this issue Sep 18, 2019 · 11 comments

Comments

@daliusd
Copy link

daliusd commented Sep 18, 2019

Using {#each...} with list/array from store updates all items from list while this can be avoided with immutable writable stores. I have expected that using <svelte:options immutable={true} /> would allow that (as per documentation "you never use mutable data, so the compiler can do simple referential equality checks to determine if values have changed" here https://svelte.dev/docs) but that's not the case.

Here I have created sample https://github.com/daliusd/svelte_immutable_store app to demonstrate the problem. Run it, open dev tools and modify text or or add new item. You will see that afterUpdate (https://github.com/daliusd/svelte_immutable_store/blob/ebc1bd4bd00a3d4a7687f5b13d484fd0135e6a1d/src/Item.svelte#L8) is called for each item even if we are updating only single item.

Here you can see https://github.com/daliusd/svelte_immutable_store/blob/master/src/store.js that I do not mutate my data thus references will not change.

Describe the solution you'd like

I think writable store should respect <svelte:options immutable={true} />. I think this might be not enough however as internal svelte code might not handle lists/arrays properly. I have not looked that deep into Svelte's code.

As well I think that by default svelte should create mutable stores (as it is now).

Describe alternatives you've considered

In some old version svelte supported immutable stores (see #1146). But I assume that was removed somewhere in progress. I think respecting global setting is better option as that would make app consistent and developer shouldn't think if this store is mutable or immutable.

How important is this feature to you?

Modern browsers seems to handle updates that do not change anything quite well. E.g. if you change div's position to the same value there will be no physical re-render. That makes this problem quite low priority IMHO.

From other side in one app of mine I do canvas re-render after update. That means I can not do re-render after each update because afterUpdate (or whatever is reactive) is called for all items even if I update only one item. I found workaround for this problem but it is second workaround I have to do in Svelte (bug is reported for first one already by other people).

Additional context

As well I see #2171 which explicitly states that stores are always mutable.

Lastly, in svelte's source code I see those functions. That's just my curiosity:

export function safe_not_equal(a, b) {
	return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}

export function not_equal(a, b) {
	return a != a ? b == b : a !== b;
}

Now I wonder why we have this part a != a ? b == b : a !== b. a != a is true only for NaN, that means this line overrides/inverts NaN behavior. Is that intentional?

@neuronetio
Copy link

I've created a lib for this purpose :)
https://github.com/neuronetio/deep-state-observer

I had a similar performance issue and didn't want to update the entire list. Immutable option also didn't work for me either.

Basically you can subscribe for only one item in the array or only one node in the object.
It is mutable anyway but this is ok! - immutable data are much slower (cloning process) so for bigger datasets it is useless and shitty. Immutability prevent mistakes that are hard to find but with deep-state-observer you can track everything easily and be informed of every change.
You have much more grained control and you can choose what to update and what to leave.
Additionally this lib has other useful features.
Right now I'm working on big project and it work like a charm.

@daliusd
Copy link
Author

daliusd commented Sep 18, 2019

@neuronetio

https://github.com/neuronetio/deep-state-observer

I have checked it out, but I don't see how it could solve my problems. E.g. I don't see how to use it to stop svelte updating the whole DOM tree. As well, I feel that even if that would work out for one of my problems it will end-up with complicated and hard to maintain code.

immutable data are much slower (cloning process) so for bigger datasets it is useless

I guess that depends on data you have. I had no such problem yet. Immutable data has other benefits like almost free undo/redo implementation if you need it.

@neuronetio
Copy link

neuronetio commented Sep 18, 2019

@daliusd yeah documentation is crappy right now
for example when you want to update one element in the array for example
instead of creating new array and rerender whole list you can modify only one portion of one array item

// instead of mutating and rerendering whole object
let state= writable({arr: [
  { id:1, enabled: true},
  { id:2, enabled: true}
]});

state.subscribe(value=>{
  // some time consuming calculations here (will be triggered when enabled property is changed)
 // all list item components will be rerendered
});
state.update(value=>{
  value.arr = value.arr.map(...);
  return value;
})
// you can do something like this
let state = new State({
  obj:{
    1: {id:1, enabled: true},
    2: {id:2, enabled: true}
  }
})

// and then in each list item component
export let row;

let grayedOut = false;
state.subscribe(`obj.${row.id}.enabled`, enabled=>{
  // do little calculation and only one component is rerendered
  grayedOut = !enabled;
});

<div class={grayedOut ? 'gray' : ''}></div>

// somewhere in code
state.update('obj.1.enabled', false);

Yes, it may look that code will be a little bit more complicated due to fact that you have grain control over what you subscribing of and what you are about to update - but it just looks like that - in real world you will know where and when some code depends on some state - it may clarify what you are doing.

You can also undo / redo state here but instead of cloning whole state each time when something tiny has changed (a loooot of memory) you can store in history only what changed end reverse the process - so you can see what exactly was changed - no need to visually search what changed in sate.
If you change state and don't save it in history it is removed by garbage collector which takes a lot of cpu resources too.

@PatrickG
Copy link
Member

It works, if you set immutable to true in the Item.svelte.

https://svelte.dev/repl/ef5c0d34d659467eb1ea7e826fb475ab?version=3.12.1

@daliusd
Copy link
Author

daliusd commented Sep 18, 2019

@PatrickG, adding this to each component would be inefficient. But anyway thank you for your comment as this pointed me to idea to add immutable: true, into rollup.config.js, e.g. after this line:

https://github.com/daliusd/svelte_immutable_store/blob/ebc1bd4bd00a3d4a7687f5b13d484fd0135e6a1d/rollup.config.js#L20

That does the trick as well.

@daliusd daliusd closed this as completed Sep 18, 2019
@daliusd
Copy link
Author

daliusd commented Sep 18, 2019

@neuronetio

@daliusd yeah documentation is crappy right now
for example when you want to update one element in the array for example
instead of creating new array and rerender whole list you can modify only one portion of one array item

This example is good if all your are interested is a changing of item but I still don't see how it would work in context of svelte with {#each ...} and or beforeUpdate/afterUpdate.

Yes, it may look that code will be a little bit more complicated due to fact that you have grain control over what you subscribing of and what you are about to update - but it just looks like that - in real world you will know where and when some code depends on some state - it may clarify what you are doing.

Well, I will always prefer less code over more code if there are no other serious differences in performance, memory consumption or code's complexity.

You can also undo / redo state here but instead of cloning whole state each time when something tiny has changed (a loooot of memory)
You are wrong here. Most probably there will be no huge memory consumption. Yes, there is some extra usage of memory, but you really should understand your data structures and choose appropriate solution. Immutable store is quite practical in most situations.

E.g. let's say I have array of objects with some objects as properties:
z = [ { a: {}, b: {}, c: 'text' }, .... ]

Let's assume it takes z.length * 200 bytes of memory. Now modification of n-th item in z will look like this:
[ [...z.slice(0, n), { ...z[n], c: 'change' }, ...texts.slice(n + 1)];

This will take (z.length * 8 + 24 + X) bytes of memory, where X is memory used for mutable change and 8 is assumed pointer size (and some assumptions made about JS behavior, most probably not very good). This converges to 8/200 or 4% of original object's size. If we don't want to be that optimistic we can assume 10% of object's size. This still potentially is acceptable it most situations.

you can store in history only what changed end reverse the process
And you need to write extra code for reversal for each operation. Sometimes that's acceptable.

so you can see what exactly was changed - no need to visually search what changed in sate.
In undo/redo case usually you don't care what has changed. If your case requires that, then go for it.

If you change state and don't save it in history it is removed by garbage collector which takes a lot of cpu resources too.
It depends. Practically I have not see big problems with this.

@neuronetio
Copy link

Yeah, maybe you have right - for very simple components dot notation is useless but if you have large app, things can get messy with "native" writable.
For bigger apps/components deep-state-observer produces less code and this code is a lot easier to understand (less error prone - no immutable combinations)

Take a look at this example, this is a simple object - but what if app/component state had a lot of different nested nodes?
I don't believe that writable and immutability can handle this easily = hard to maintain code = buggy.
https://svelte.dev/repl/3e9f95108e5f44c0921b1c3fab8574e7?version=3.12.1

@daliusd
Copy link
Author

daliusd commented Sep 19, 2019

Yeah, maybe you have right - for very simple components dot notation is useless but if you have large app, things can get messy with "native" writable.
For bigger apps/components deep-state-observer produces less code and this code is a lot easier to understand (less error prone - no immutable combinations)

I still don't see how deep-state-observer will solve problem with re-render of lists/array using each :-)

New question: what happens if I create 100 or 1000 deep-state-observers? How expensive performance wise is it?

Take a look at this example, this is a simple object - but what if app/component state had a lot of different nested nodes?

Normally you keep your structure as flat as possible. Here is article about redux https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape but the same principles applies to any immutable store/state. KISS.

I don't believe that writable and immutability can handle this easily = hard to maintain code = buggy.
https://svelte.dev/repl/3e9f95108e5f44c0921b1c3fab8574e7?version=3.12.1

I am working on large project using React with Redux. Usually no big problems with that. That's anecdotal evidence but still...

@neuronetio
Copy link

I still don't see how deep-state-observer will solve problem with re-render of lists/array using each :-)

https://svelte.dev/repl/a3637b5e83914c9b89f10c8cd422c747?version=3.12.1

New question: what happens if I create 100 or 1000 deep-state-observers? How expensive performance wise is it?

It depends. deep-state-observer is couple times less expensive than cloning whole object if object has ten thousands of records, but when object is small cloning is faster (but with small object it doesn't matter because two paradigms works real fast).

Normally you keep your structure as flat as possible. Here is article about redux https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape but the same principles applies to any immutable store/state. KISS.

Yeah you keeping it flat because it is hard to work with structured data when you want this data to be immutable :D Sometimes flattening your data can lead to ugly names of properties and not quite well organized data (of course it depends). I like to have things organized and structured into proper nodes (like folders). Normalized data from your link is of course better it is grouped well, but it is easier to access and set data with dot notation - less code :P
With dot notation you can use immutable data too - on specific nodes so it is elastic - you are not replacing whole object - only specified items.

I am working on large project using React with Redux. Usually no big problems with that. That's anecdotal evidence but still...

It depends on your project and on your habits.
For me it is easier to use dot notation than writing a lot of actions for store (just for immutability).
Those actions takes time and I think for simple data update this is an overhead.
I think that dot notation is simpler and easier to refactor - you have path and you can always easily change path even with search and replace. But when you are working on live object you need to change a lot more code. Path here are kind of lenses in functional programing - you need to change string - not code itself.

@daliusd
Copy link
Author

daliusd commented Sep 19, 2019

Thanks for example and your responses. I have learned something new (I think others as well).

@daliusd
Copy link
Author

daliusd commented Oct 18, 2019

I have a SVG with N items (rectagles, images, ...) and, when user "clicks" one of this items, I show a rectangle with "sliders" that user can use to rotate/scale the object in-place.

Svelte is perfect because each change in the model of the item is inmediately reflected on DOM.

I want to introduce "store" philosophy just to decouple model interaction with my components structure using subscriptions to detect changes (like "a new object has been inserted").

Problem with immutable store philosophy is I have tu "redo" the items array completly each time an small change is performed and this will cause svelte not to manage SVG Dom structure optimally.

¿Is there a better organization of the store to manage fine items operations like "item removed", "item added", "item updated" in a non inmutable way?

By default svelte's stores are mutable. So you are good to go without recreating everything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants