-
-
Notifications
You must be signed in to change notification settings - Fork 407
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
Tracked Storage Primitives #669
Conversation
e04fdce
to
2c77fc2
Compare
`createStorage` can receive an optional initial value as its first parameter. | ||
It also receives an optional `isEqual` function. This function runs whenever the | ||
value is about to be set, and determines if the value is equal to the previous | ||
value. If it is equal, it does not set the value or dirty it. This defaults to | ||
`===` equality. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this! 👏
sometimes they aren't the right tool for the job. For instance, you may need | ||
have a problem that requires you to work with a number of dynamically created |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sometimes they aren't the right tool for the job. For instance, you may need | |
have a problem that requires you to work with a number of dynamically created | |
sometimes they aren't the right tool for the job. For instance, you may | |
have a problem that requires you to work with a number of dynamically created |
about the details of tracking state. This is a bug-prone process, because it | ||
only takes one mistake in one usage, and the state itself could be used in | ||
many places. In Octane, users should only need to declare reactivity once - | ||
when the state itself is defined. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yaaaaas : 💯
I have a use case for this (dynamic tracked values), so I'm looking forward having it available. |
Can an example of |
@mixonic it is mentioned in the API description/docs, and it is used in the re-implementing tracked section and the final example of the Creating custom tracked data structures section |
I'm in favor of introducing these primitives and like this design in general, but would like to reconsider one aspect of the design to both be more consistent with the higher-level Specifically, I propose that we change the name and default value of the parameter |
@dgeb I agree with the This RFC is essentially about rolling back that pattern in favor of creating tracked data structures instead. As noted in the RFC motivation and the changes to the guides, the old resetting pattern had a number of problems and issues, and using tracked data structures prevents these issues from occurring in general. With this in mind, the fact that resetting a tracked property triggers a notification actually enables antipatterns, so ideally we would change the behavior at some point in the future. That said, this behavior has proliferated already, so we can't change it without some sort of deprecation and roll-out plan, and possibly an optional feature. However, we can change the default behavior of new tracked features, such as this one, which is why I made the default stable under |
@pzuraq Thank you - the name change was my primary concern. I can accept and understand using a different default check at the primitive level.
Certainly, the behavior chosen for |
@@ -461,8 +454,8 @@ Now we have a storage that we use to represent the value of the collection | |||
itself. We pass a custom equality function to this storage which always returns | |||
`true`, meaning that whenever we set the value of this storage, it will _always_ | |||
notify, even if the value hasn't changed. We then entangle the collection | |||
storage in `forEach` by using `getValue` on it, even though we don't actually | |||
use the value, and we dirty with `setValue` whenever we `set` a value. Since we | |||
storage in `forEach` by hgetting its value, even though we don't actually |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/hgetting/getting/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit concerned that the usage of Storage
conflicts with the web storage API, obviously in our applications you'd be importing it but if you forgot to import it you would be fairly annoyingly trolled (it would not lint against it, it would seem to "be a function", but it just isn't what you think it is)...
775b6be
to
5b7c385
Compare
I would like to explain my experience with migrating our core model layer to use auto-tracking instead of computed properties. I got a lot of help from @pzuraq so big thanks to him! We have a single getter and setter pair which we use for all properties in our models. They dynamically dispatch to the correct place based on the property at hand. There are two specific use-cases that we had trouble with.
We've currently worked around these two problems by maintaining an untracked copy of the data. That, however, lead to a bunch of pretty nasty code in some places and is plain impossible in others: imagine that we pass a deeply nested value from the tracked data through a bunch of components to the last one which modifies it in a two-way bound All of that leads me to say that if we had a |
I second @boris-petrov suggestion to add That all said, I am very much in agreement that Ember should lead devs on the happy path and I think @pzuraq's concerns shouldn't be ignored! Based on the rationale in this RFC, it sounds like the only concern with adding TL;DR. I think Boris's examples feel very normal and thus strongly second providing |
@scottmessinger @boris-petrov I am not the only person on the core team with these concerns, and I would say that we are generally aligned in this perspective. It's going to take significant evidence of crucial use cases that are common and widespread to change this view. The types of failures we expect to arise if we were to allow users to easily This works in the Rust community because:
I think that if every Rust library were to start writing It's an even worse sign that the common first question when someone runs into the backtracking assertion is "how do I peek?". If we make it available, even in a way that suggests it is dangerous like @scottmessinger suggests, I think it would likely be abused immediately because it does seem like a simple, easy thing to do, and the bugs it could cause are not readily apparent. This is why I think the status quo, where someone who wants to have this capability has to do a large workaround like @boris-petrov laid out, is actually ideal. FWIW, we do not peek values even inside of Ember/Glimmer. We follow our own rules here! So I think this is very possible to do in general. So, having said all that, I've discussed this in detail with @boris-petrov offline, and if I remember correctly there are two use cases that he has:
For the first issue, this is solved by this RFC. For the second one, I think the key thing to focus on is the fact that this is about initializing state. During initialization, you are creating a tracked value for the first time - you shouldn't actually have to read it. I actually think the ideal solution here would be to manually create and manage tracked storage instances within this data layer. The first time value is accessed, you create the tracked storage, and initialize it to the default value. This would likely be more efficient overall than using |
For what it’s worth, as the Octane migration lead for LinkedIn.com, I see these kinds of issues all the time as folks migrate from Ember Classic patterns—and precisely because of that experience, I strongly agree with @pzuraq and others on the framework team. Accordingly, and without digging into this specific example, I’d like to offer a general comment here in hopes that it’ll be useful more generally for the community thinking around migrating to Octane. Fundamentally, Octane entails a new programming model—one that is not a direct translation of the old model into some new syntax. That means that some patterns that you’re used to using in the Classic paradigm simply do not work in the new paradigm (though not very many). However, in literally every single one of those I’ve hit over the last year, I’ve ultimately been very happy to end up doing the work of substantially rethinking and reworking whatever abstraction or implementation was at hand. In practice, that has often meant:
(4) has been rare, but has occasionally happened. For example, over the summer we took a system that made heavy use of The single most common example of this kind of rewrite for us is figuring out how to replace uses of This process is both harder and slower than just trying to translate directly from Ember Classic into Ember Octane syntactically, to be sure! It has also paid off handsomely, though. All of this to say: it’s very common to find your existing APIs and solutions to a problem aren’t Octane-friendly. One response is to say this is a problem with Octane. Depending on your views on software design, that might be fair! Another, though, is to say that it’s possible the abstraction would benefit from being reworked substantially. This is my POV, and again: every single place we’ve hit like this in our app has been improved by the rewrite—often massively so. That doesn’t mean Octane is perfect. For one, there are still gaps: this RFC addresses one, and the work to get resources and effects addresses another. For another, observable-based systems (like Ember Classic) and incremental computation systems (like Ember Octane) simply have different tradeoffs and affordances—and you might prefer the former! But the fact that a pattern that worked in Classic doesn’t work in Octane doesn’t mean that Octane is wrong or even that it is missing something. It does mean that it makes fundamentally different tradeoffs in the design space than Classic did. Me, I prefer the Octane flavor of the tradeoffs, and when you hit spots like this I encourage you to rethink the design. You may find yourself in a place that surprises you with how much cleaner and more maintainable it is! You may also occasionally find a spot where the tradeoffs are a little worse (hasn’t happened to me yet, but I’m sure it’s possible). But either way, the fundamental design is the way it is—and breaking the core abstraction would be much worse. Edit, 2020/12/07: I turned this into a blog post. |
We discussed this at todays core team meeting, and are in favor of moving this into final comment period. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed today, and we are ready to merge.
@pzuraq - Mind fixing up the frontmatter linting issue and then merging?
1dc38fc
to
2845588
Compare
tracked storage. | ||
|
||
```js | ||
function tracked(target, key, { initializer }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the target
argument is not used in the function, is this intended?
Rendered