-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
consider migrating from Immutable.js "Records" to plain objects #2345
Comments
Wow that's a tough call... For what it's worth, the Immutable Formatter extension doesn't work in Electron, so Immutable does make debugging quite a bit more difficult as I have to use |
@slapbox yup, very tough call. If we were starting fresh it seems like it would be a no-brainer, but hard to gauge benefits vs. costs now. Thanks for the input though! Anything is helpful, just trying to kick around ideas. |
I’m no expert in Slate, but I like the changes you’ve posted to the rich text example. Personally I think removing dependencies and going closer to the metal is good for a project like Slate. I can’t help with the codebase but I could help with the docs and examples if it’s worth anything. |
Personally the learning curve for Immutable wasn't that steep. I must say that I have already scratched the surface of Immutable a few years back, and then decided to not use it, so that may have eased my learning curve. But now that Immutable comes in a package deal with Slate, I find myself using quite some Immutable functions already, like value.document.nodes.get(-2) to get the second last node. This can also be achieved with (slightly more verbose) plain javascript or an imported utility library but it would definitely be quite a rewrite. I expect I'm not the only one who would need to rewrite quite some bit. But I completely agree with the CONs. Especially the argument that by forcing people, that are not familiar with Immutable, to use it anyway, they will probably not use 95% of its functionality. One of the reasons I fell in love with the React ecosystem, after working with Angular, was that you needed to include small libs for almost everything yourself, so I could pick only what I thought was best for the project. On one hand I'm wondering if this change is really needed on the short-term, since it will probably delay v1 by another few months, without really adding new functionality (rather removing it). But on the other hand, my fear of needing to rewrite quite a bit of code only supports making this change now, instead of in between v1 and v2, because then it might impact even more projects in production. |
In my opinion we need to focus on stabilising what we have now. There has already been two heavy refactors lately. Those were needed and gave very clear benefits right away. This one I'm not so sure about (I agree with much what @grsmvg said). I see why we would want it though, but I think it would be better doing it in a v2 when things are more stable all over. Besides I think such a re-write would take a lot of focus and energy away from bug-fixing, cross browser compatibility (as fore slate-react), and general UX concerns. And introduce a whole lot of new bugs in the re-write phase. |
A point you did not mention: not using Immutable will also reduce the size of Slate's dependencies. If you are not already using Immutable in your app, it adds 55kb (minified, not gzipped) of JS to your bundles. It is really best for "core" libraries like Slate to come with as small as possible overall size (dependencies included). |
IMHO, we can go further : I think that an entire refactoring of the internal Slate state would worth it if it includes CRDT considerations. I didn't dive inside that part of the code, and I may be wrong, but I'm not sure that 2 updates operations (add or delete) could be switched safely ; there is no notion of which user session did which change. We need this because undo/redo is tightly coupled to its owner (think about user experience : what if you concurrently edit things and when you want to undo you change, it would delete another user addition ?) If you just replace Immutable with whatever, you'll have to replace later that whatever with a CRDT-friendly library if you intend to supply real-collaborative edition :
Maybe the latter is the more suitable. |
Is this something we could just achieve with build in javascript classes (which support getters)? If so, could Immer support them? |
In fact, to go on with my previous idea, Immutable (or Immer, that does the same but differently) just changes the state of a single client, whereas it doesn't make sense to manage immutable objects because an undo operation is no longer sequential when several people are editing collaboratively the same document. We don't need to replace Immutable, we need a new strategy for having a data structure that accepts updates in orders that may vary according to each clients. |
Ian suggested (#259 (comment)) that Slate should be unopinionated about OT/CRDT considerations. I'm not sure it is easily possible : if Slate data model is aside CRDT data model, you have to maintain both together (like with the Automerge example), and as far as I know, the Slate data model doesn't consider absolute reference of its atoms, which is the key point in CRDT : the problem with indexes is that they shift, how would you find the location of a Slate change in the counterpart model ? That said, I'm not well aware of the internal Slate data model (perhaps I'm wrong) ; as mentioned previously, it is hard to read because it relies on Immutable and I don't know that library. |
That's true, there are definitely some methods in Immutable.js that are useful. But it is definitely something that most people never find. Whereas if we using plain JavaScript data structures people would end up using
I understand where you're coming from here, but there is no timeline for
This is fair, and I'm okay with waiting a little bit so that we can get the current
This is a great point! Not only by being a dependency, but the extra bloat it adds to defining records would also reduce some of the core Slate bundle size too.
This is something I leaned towards at first, when we were focused mainly on Immer as the main question. But as it evolved, I realized that most of the benefits in terms of performance and simplicity actually come not from Immer, but from being able to use plain JavaScript objects to avoid parse/serialization steps, and to avoid having two different representations for objects. At this point I'm more interested in the plain data structures than Immer itself. If there were some (unknown) better library that Immer, I'd use it while keeping the plain data structures. @ppoulard I'm interested in Slate being able to support CRDT, however I have yet to see a CRDT approach that I think feels viable for nested documents and for real-world use cases where documents don't balloon to huge sizes. For that reason most of Slate's collaborative support has been focused on making OT possible first, which it already is today with the current codebase. (And it still would be with plain data structures.) Since CRDT and collaboration is pretty convoluted, and seems so far like something that many people want, but few people actually have the time to do, or don't want to put in the effort, it's not something that I'm going to be personally adding to core. And for that reason I don't want it to hamper any decision we can make here. If it turns out to absolutely be the best way, and it turns out also that we absolutely have to use non-plain data structures, we can cross that bridge in the future when someone puts in the work to add CRDT. (But it might be very far in the future.) Thank you everyone for your responses! (And feel free to provide more if you think of other things too.) The more I think about this, the more it seems like something that would be very beneficial to Slate. The tougher part is that decoupling from Immutable.js would be harder in cases where its custom (helpful) methods are used. That's the biggest blocker in terms of a refactor. I'm okay with waiting a little bit on this now, to let the current version of Thank you! |
Thinking about it more, I think the best way to make this change would be to do it in two stages:
|
If you go with those big changes, might I suggest starting to use Typescript for Slate's core? It is not hard to incrementally use typescript nowadays as its only a Babel plugin (I did a coffee => js => ts migration for my app incrementally, I dont regret it at all and both can really cohabit). |
The JS community moves very fast and many projects have suffered from coupling to solutions that died or simply went out of fashion (eg: Coffee script). By using native structures Slate will be able to better stand the pass of time. |
@renchap I'm open to using TypeScript, but I'm not sure I want to couple it to this idea. But if someone wants to help TypeScript adoption, I think a decent next step would be to open a pull request with a single small-to-medium-sized file converted to using types and we can have a discussion around it there. |
@ianstormtaylor The thing with moving to Typescript is to start to type your core model first. Once it is typed, you can start to type the functions that are using it, and expand. This allows you to avoid putting a lot of I will try to find time to open an experimental PR with this. |
Hi, may I know how we can deal with cache with immer? I am afraid of something like this in user side:
|
@zhujinxuan that's a good question, I'm open to ideas. Thanks to Immer, I believe all of the core JSON models would be frozen in development mode, so they can't be mutated. We might want to do a similar freezing for return values. For caching, one thought was that potentially the only cache that truly matters is the For the But I'd love to hear other ideas here! |
@ianstormtaylor BTW, there are two caches. The other is I think it is a good idea of global I am not sure whether it is a good idea to have an global |
@zhujinxuan that's a good point, it could not be |
@ianstormtaylor Great. I will open a experimental PR to replace the current |
Just came across another place where we are weirdly using the two different representations... the Right now if you return |
@ianstormtaylor That's why types are so helpful. Can't imagine writing code without them anymore. For newcomers, I highly recommend learning algebraic polymorphic types. It's really a game changer. First in Flow, but TypeScript supports them as well https://www.typescriptlang.org/docs/handbook/advanced-types.html It's also useful to know https://flow.org/en/docs/lang/nominal-structural/ types. Because Slate can be written as classic OOP class & inheritance "hell" or as plain data with algebraic types (which really rocks). Until you will feel safe when thinking about that, I would not recommend using any typed language abstraction over JS. Example https://gist.github.com/hallettj/281e4080c7f4eaf98317#file-tree-no_classes-js-L3 I am afraid you will have a desire to rewrite everything once you will switch to types. Because types work the best when everything is typed. |
Btw, asked Lee Byron https://twitter.com/steida/status/1058442668911529984 what he thinks about immutable to Immer, and he suggested Immutable 4. |
Hi, I think @thesunny 's idea on using a
Then what we shall return for We can allow the bold mark proxy visit mark properties just like mark, but it will require us define proxies for our current models for chain call. |
Can anyone comment on what kind of difference this would make in node rendering speed in terms of percentage improvement? I pretty much never see performance issues with Slate itself but rendering a document for the first time can have some slight delays. The slowdowns are a lot more significant on mobile but pretty much imperceptible on desktop. |
@zhujinxuan yeah, my proposal is syntactic sugar primarily designed to make things a little easier to read while still respecting the idea that all the values are pure JSON. It's not a very good replacement for models with methods. The equivalent syntax would be something like: $.getActiveMarks(value).find(m => m.type === 'bold')
$.getActiveMarks(value).find(m => m.type === 'bold').anchor
const marks = $.getActiveMarks(value).find(m => m.type === 'bold')
$.doSomething(marks) Another proposal could be to wrap the JSON before calling the methods and I actually kind of like the readability of this. I was going to propose this as well originally: $(value).getActiveMarks().find(m => m.type === 'bold')
$(value).getActiveMarks().find(m => m.type === 'bold').anchor
// option 1
const marks = $(value).getActiveMarks.find(m => m.type === 'bold')
$(marks).doSomething()
// option 2
$($(value).getActiveMarks.find(m => m.type === 'bold')).doSomething() The second syntax is straightforward to implement which is nice: class Node {
getActiveMarks() {
// ...
}
}
class Document extends Node {
}
const CLASS_LOOKUP = {
node: Node,
document: Document,
}
$ = function (value) {
const TheClass = CLASS_LOOKUP[value.type]
return new TheClass(value)
} |
Hmm... and yet a third way is to always return the values wrapped automatically and have the data always kept in a JSON value. This way it's easy to convert back and forth without any work. In other words: $(node).json === node Or to get active marks, do something with it, and then get the resultant JSON value: $(value).getActiveMarks().find(m => m.type === 'bold').doSomething().json |
It feels like I'm having a discussion with myself but I have been thinking about the alternatives and the trade-offs. The three alternatives are:
I'm gravitating towards the
|
@thesunny - I've been following the discussion; I just don't feel that I'm in the position to make good design choices in this situation. Keep up the good work! |
Same here, following all Slate discussions but due to time and experience constrains can't always contribute. If it helps, my personal use with Slate Editor, requires better performance on large documents; bundle size is a non-issue. My Slate package is way out of date so I don't mind fixing incompatibilities either, as long as they aren't too crazy. I'd imagine this switch would be the largest breaking change before |
@thesunny I am thinking whether we can use a proxy interface for that. For example
shall return a pure node. However, I am feeling adding a new proxy on top of immer proxy only make things more complex. |
@zhujinxuan can you expand on your comment? Can you put the equivalent code (either current or as proposed by thesunny) for what you're proposing side by side? I'm not clear on what currying |
I think using just |
I like |
Could we start by mimicking most of the API with functions and getters on the object directly? Fake example API:
|
@ianstormtaylor what's your current position on this matter? has anybody started working on this migration? |
@dragosbulugean good question. For the next steps on making this happen, check out: #2495 — a list of the smaller steps that go into being able to make this change. If anyone can help with any of the steps (or happens to discover another thing we need to fix) that would be awesome! As soon as we have the precursors done we can make the breaking change. |
Question: What is the benefit I'm just playing devil's advocate - it could very well be that deep changes are too annoying to to manually, or that Another potential problem is that * freezing might incur a performance penalty, at least in 2017 |
I believe I’m unsure of what the browser support is. But we don’t want to re-invent the wheel and write the immutability logic ourselves when a popular existing solution with no external API impact exists. |
Directly mutating objects is much more natural than immutably updating objects. This is presumedly why Immutable.js was chosen in the first place, because it offers an API which feels like direct mutation.
Immer automatically falls back to a getter/setter-based solution, much like Vue or Mobx. Source code here: https://github.com/mweststrate/immer/blob/0ad9a91e189685b24714686bd6a42699ce02feae/src/es5.js |
Immer already freezes objects only in dev mode
I feel that properly dealing with deep updates would end up with rewriting Immer |
I had a look at Immer, and I think I agree. The main benefit of Immer would be doing multiple mutations in one go, which would either not be implemented or recreated when doing immutability manually. The only way to really be sure is to implement it both ways and benchmark, which I think would show trade-offs on both sides. I resign as devil's advocate ;) |
I've actually done benchmarks of immer vs ramda vs plain JS for deep immutable updates. I don't have the benchmarks around anymore, but all solutions were on the order of tens of thousands of updates per second and trading blows for the top spot. In terms of performance, there was no clear winner. In terms of simplicity, however, there was a clear winner. I think it's obvious by now which one I like. 😄 |
We should definitely limit the breaking changes as much as possible for the end users. Using JS However, I do agree ImmutableJS is a pain, especially server-side. Have to give us on using the Value.fromJSON() because of that and now I work just with the JSON tree. |
Immer supports classes now, FYI: https://github.com/mweststrate/immer#supported-object-types. There is also a low-level |
I forgot, one of the issues in terms of removing keys from nodes, is that when rendering in React-land there's no stable key to use, which leads to issues. |
Another thought: I think once we’re using JSON-based models we’ll move fully to interface-based helper functions. Right now things we have “mixins” that approximate this by attaching common methods to the prototypes. And we can rely on instances to do But in the future we’ll be exposing a bag of helper functions that operate on plain objects and arrays. We could continue to require that those objects have an Since decorations and annotations both implement the |
Fixed by #3093. |
Do you want to request a feature or report a bug?
Discussion.
What's the current behavior?
When Slate was first created, Immutable.js was the best and most popular way to handle immutability in React. However, it has many downsides for our use case:
CON: It requires a
fromJS
step to build the collections/records. Since the objects aren't the native JavaScript data types you get from JSON, we have to have an interim step that instantiates the Immutable.js data model. This can be costly for large documents, and there is no way around it. This is especially problematic in server-side environments where serializing to/from JSON blocks Node's single thread.CON: Reading values is more expensive. Immutable.js is optimized for non-crazy-slow writes, at the expense of read operations being much slower than native JavaScript. Since Slate's model is a tree, with many node lists, this ends up having a significant impact on many of the common operations that take place on a document. (See this question for more information.)
CON: It introduces a fairly steep learning curve. People getting started with Slate often get tripped up by not knowing how Immutable.js works, and its documentation isn't easy to understand. This results in lots of "wheel reinvention" because people don't even realize that helper methods are available to them.
CON: It makes debugging harder. In addition to a learning curve, debugging Immutable.js objects adds extra challenges. There's a browser extension that helps, but that's not good enough in lots of places, and you end up having to us
toJS
to print the objects out which is very tedious.CON: It increases bundle size. The first increase in size comes from just including the
immutable
package in the bundle at all, which adds ~50kb. But there is also a more insidious bundle size increase because the class-basedRecord
API encourages monolithic objects that can't be easy tree-shaken to eliminate unused code. Whereas using a utility-function-based API, similar to Lodash, would not have this issue.However, it does have some benefits:
PRO: Custom records allow for methods/getters. This has been the primary way that people read values from Slate objects. Because Slate (and rich text in general) deals with some complex data structures, having common things packaged up as methods allows us to reduce a lot of boilerplate and reinventing the wheel for common use cases.
PRO: It offers built-in concepts like
OrderedSet
. These allow for more expressive code in core than otherwise, because we'd need to reinvent the wheel a bit to account for JavaScript not having some of these concepts. Although truth be told this is probably a fairly minimal gain.Since Slate was first created,
immer
has come on the scene (thanks to @klis87 for kickstarting discussion of it in #2190) which offers a way to use native JavaScript data structures while maintaining immutability. This is really interesting, because it could potentially have big performance and simplicity benefits for us.All of the CON's above would go away. But we'd also lose the PRO's. That's what I'm most concerned about, and what I'd like to discuss in this issue... to see what a Slate without Immutable.js might look like, and how we could mitigate losing some of its benefits.
Without the ability to use classes and prototypes for our data models, we'd need to switch to using a more functional approach—exporting helpers in a namespace, like we currently already do for
PathUtils
. One question is whether this will be painful...Looking at our
rich-text
example, we'd need to change how we do things in several places.slate/examples/rich-text/index.js
Line 54 in 8eb8e26
We no longer have getters on our models, so
value.activeMarks
doesn't work. Instead, we'd need to change this to:Similarly, there's no
value.blocks
any more:slate/examples/rich-text/index.js
Line 66 in 8eb8e26
So we'd need:
This is actually nice because we're no longer using potentially expensive getters to handle common use cases—calling functions is more clear.
But we also can't use helper methods like
document.getParent
.slate/examples/rich-text/index.js
Line 148 in 8eb8e26
Instead we'd need to use:
Similarly, we can't do:
slate/examples/rich-text/index.js
Lines 293 to 295 in 8eb8e26
And would have to instead do:
But we also get to remove some expensive code, since we don't need to deserialize anymore:
slate/examples/rich-text/index.js
Lines 41 to 43 in 8eb8e26
That's all for the rich text example, but it would definitely be a big change.
I'm curious to get other folks's input. Are there other PROS or CONS to Immutable.js that I haven't listed above? Are there other ways of solving this that I haven't considered? Any thoughts! Thank you!
The text was updated successfully, but these errors were encountered: