-
-
Notifications
You must be signed in to change notification settings - Fork 4.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
Make $capture_state capture local state #3822
Conversation
I haven't looked at the implementation in this PR, but it's not clear to me what ought to happen when using |
For HMR, we're not interested in individual props. We just want all props as a whole and, depending on user's I didn't change the existing implementation for props. It currently uses internal names. But that's an interesting observation, I think I may have overlooked the Also, I thought of another change that could be useful. As an option, I'm making this PR WIP until I've completed the tests mentioned above. |
OK so I finally wrapped my head around this... In fact, it might be better, even for HMR, to be able to capture I think the best strategy to restore a component state when it is replaced on a HMR update is the following: const cmp = new Cmp({ ...originalOptions, props: updatedProps })
cmp.$inject_state(state)
The interesting one is It would be implemented something like this (if const updatedProps = mapObject(
originalOptions.props,
export_name => capturedState[export_name]
) The alternative to this strategy would be to recreate the component with the original options, including original props, and then restore everything with I have a feeling that the first one may be better because it instantiates the new version of the component directly with the current prop values. But really, I can't be sure. Can there really be a difference? Can this trigger different side effects? TL;DR Is it possible to have different outcomes when creating a component with different prop values and then, immediately, synchronously calling the same Regarding this issue, the first strategy would need // returns only props with their export_name
cmp.$capture_state({ export_props: true })
// returns both props & local variables with their internal names
cmp.$capture_state({ props: true, local: true }) Maybe another method entirely would be cleaner? cmp.$capture_props() |
I don't think (even optionally) merging the props and internal state into one variable is a good idea. It's possible that someone could (for whatever reason) have a component where some prop and some non-corresponding internal state had the same name. I think it makes sense to have separate methods for these. Something else I noticed the other day with the current |
I have resolved the conflict with master.
I agree. So I'll focus on
Yes. I have been bitten by this. This PR filters store subscriptions out both in I've also added a test that confirms the behavior of I'm happy with the PR now, so I'm removing the WIP flag. This is now needed to fix local state support that has been broken by |
const var_names = (variables: Var[]) => variables.map(prop => p`${prop.name}`); | ||
|
||
capture_state = x` | ||
({ props: $p = true, local: $l = true } = {}) => ({ |
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.
Was this left here on purpose? What I had suggested (and what it sounded like you agreed sounded good) was to have separate methods for capturing local state and for capturing props. What I especially don't want is something that returns both at once merged into one object, as that could lead to weird shadowing.
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.
Oops, sorry. I had remained focused on the previous discussion of export { ... as ... }
. What I had understood was another method to deal with publicly visible (i.e. aliased) names, not to exclude props from $capture_state
entirely.
My current implementation only uses internal names, both in what it returns and what it consumes (like the existing one, in fact). Is there really a risk of shadowing in this case? I don't really appreciate if this state could contain other things that could conflict, now or in the future...
If we had a $capture_props
method, we'll need a $inject_props
too, right? And this pair will have to deal with aliased vs internal names. Also, we need to sort the names of the methods out because what the existing $capture_state
currently does is only exposing props as their internal names.
For now, I'm not sure HMR will ever need public names and, if so, I can workaround it with public API by using compiler's output. svelte-devtools
also seems content with current $inject_state
. So I was hoping efforts for $capture_props
or so could be deferred until it was really needed.
Do you think I should split the methods right now? What should be the naming?
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.
Someone could (although it would be crazy) do something like
let foo, bar;
export { foo as bar };
This is why I think props should be entirely excluded from the 'state' methods.
If you think we don't need $capture_props
/$inject_props
yet for tooling, that's fine, but I don't think $capture_state
should have anything to do with props.
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.
To be clear, by this I don't mean that $capture_state
should remove props from the object it returns, but just that it should disregard any export
s and return an object of all of the top-level variables apart from those with injected: true
.
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.
OK so, if I understand correctly, you're saying $capture_state
should not take any argument and always return every writable variable that is neither injected, nor a store subscription.
That seems to make sense to me so I've updated the PR to do just that, and added a reactive statement in the test case to ensure that they don't end up in the captured state.
This is looking mostly good but one small issue. I would like to be able to read $ prefixed variables. Setting not so important but in devtools the ability to see what value is being pulled from the store is very useful. |
Updated as per @RedHatter suggestion, since @Conduitry said earlier that exposing store subscriptions in Since @RedHatter What solution do you have currently for handling aliased prop names (export as)? Isn't this a need for devtools? |
You can always synchronously get the value of a store, but I suppose I can see why it would be nice if that was exposed directly, rather than making the dev tools do the subscribe-and-unsubscribe. I'm starting to wonder now though whether it's really best to strip out all injected variables. If you have something like And now I'm wondering about how safe it is to just inject arbitrary state at all. If you have |
It could have side effects to do so, couldn't it? I mean, if we just have a
Yes, indeed. If we can't exclude the semantically equivalent
Not for me. Actually, in the case of HMR, a component could have some props / local variables removed from the component after an update. That means that the component would be injected some extraneous state during normal operation. In this case, it is the intended behavior to ignore it. In my opinion, what we can do is just expose about everything from That means Moreover, For HMR, there might be some weird edge cases where a reactive value computation dependencies change to some state that was not present in the previous version of the component... In this case, the reactive value will have the old value injected and, I guess, won't be recomputed since @Conduitry Any insight about this? What do you think should be the next step for this PR? |
Yeah I think now it makes sense for I think it makes sense for I also suppose that we shouldn't worry too much about the component getting into a weird state because of these methods, because whenever you start mucking around in devtools that's always a distinct possibility. In short: Yeah sounds good. I think we might be close to the finish line on this finally. |
I use
I'm not 100% sure about this. Consider the following.
It might be useful for a user to be able to see the value of
but maybe not. Honestly could go either way. Would including non-writable variables be an issue? They would still be filtered out by |
@rixo I'm probably not the right person for this, but I think this a huge step forward. I'll see if we can get it in front of the right people when they have some available time. |
const has_invalidate = props.length > 0 || | ||
component.has_reactive_assignments || | ||
component.slots.size > 0 || | ||
capture_state || |
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.
Is the intent here (and then again below) to check whether there's a non-noop capture_state/inject_state? Right now, won't this will be truthy whenever we're compiling in dev mode?
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.
Hm when I made a change to this effect locally, what ended up happening was that in the debug-no-dependencies
test, no instance
function was generated at all, and so $$self.$capture_state
/$$self.$inject_state
weren't set, even to noop
. So perhaps what's here now is correct.
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.
An alternative would be to make $capture_state
and $inject_state
actually exist as instance methods on SvelteComponentDev
(as noops), and have them be overridden in instance
when necessary - which I might actually like better. Thoughts?
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.
Hey, thanks :)
So yeah, it was always true in dev mode (before your change) but I just wanted to write out the real reason, that it was needed for $capture_state
and $inject_state
.
I agree that having them on SvelteComponentDev
is far better. It also explicits these methods' contract by making it clear that they are always present for all components in dev mode. It's great.
I am interested in seeing this land. Is there any way I can help test this? Do you have steps to test? |
@F1LT3R I've passed it through the HMR test suite, and this branch fixes all the test cases for preservation of local state and reactive blocks that are currently failing with master. As far as I can tell, the test suite is otherwise pretty unforgiving, so I'm pretty confident this PR gets us covered regarding HMR needs. Are you asking out of interest for HMR or dev tools, or are you planning on using this in some tooling of your own? |
🎉 This has been (finally!) released in 3.19.0. Thank you for your work towards making HMR a reality, and thank you for your patience during the long life of this PR! |
Previously, these methods only applied to exported props. Also, add $$inject option to constructor, which injects state before running the update loop.
@Conduitry --- sadly, this commit breaks using Babel macros in Svelte code.
Appearances of the I'll continue looking into how I can unbreak my code. Any ideas? |
Hm that's not quite the same situation, and won't be addressed by that PR, because in your case |
I'm afraid it's not the same thing as #4463, As I get it, Babel sees something like: $$self.$capture_state = () => ({ foobar }); and turns it into: $$self.$capture_state = () => ({ () => { console.log('I am a macro') }) @spacedentist Can you give more details, in particular the error you get and, if possible, the generated code of the component? Unfortunately, I suppose Svelte would see this variable as referenced at compile time, before Babel intervention (except the user is dull and imports things for no reason, or I'm missing something with macros). My first intuition is we should not capture imported things, but I don't see how to know that at the place we're generating the code for capture/inject state currently. Is there a way? |
If we generate $$self.$capture_state = () => ({ foobar: foobar }); It's more bits, but it's dev code, so I'd be inclined to do this as an easy fix, instead of adding code to Svelte to detect imported variables (except if the code is already there and I missed something?). |
@spacedentist I've pushed a branch with the possible fix suggested above in my fork. Can you try it, and see if it does fix your problem? master...rixo:capture-state-macros-tentative-fix You can install it in your project with: npm install -D css-tree@1.0.0-alpha22 rixo/svelte#svelte-v3.19.1-gitpkg (dunno what's going on with css-tree but you need it too) I've only fixed |
@spacedentist which babel macro are you using? It should handle when it is being used in unintended scenarios: $$self.$capture_state = () => ({ foobar }); is essentially the same as $$self.$capture_state = () => ({ foobar: foobar }); and i think the macro should either yell at it, or remove the occurrences of $$self.$capture_state = () => ({ }); |
Huh? Oh OK, Babel macros are not what I imagined they were apparently. "breaks using Babel macros in Svelte code" made me think that there was total breakage, and so that the resulting code had to have a parse error. But trying with a random macro, I end up with this code: $$self.$capture_state = function () {
return {
ms: ms,
};
}; This is indeed a runtime error ( So this could be fixed locally by changing $$self.$capture_state = () => ({ ms: typeof ms === 'undefined' ? undefined : ms }) This looks more like protecting against a bug that should have been handled beforehand than a proper solution, though. So the real fix is probably to not capture imported symbols. It also feels in line with not capturing globals to me. But that would require to add a new flag to |
@spacedentist You should probably open a proper issue with your details, if you want to see this fixed. |
Hi @rixo, sorry for not replying earlier. So, looking at your proposed commit 989803d ... that wouldn't help I'm afraid. So the thing with babel macros is that you use them like this
...which looks like you're importing a symbol from another file, but in fact that's not what happens. Essentially, what Babel does is it loads So I've written a couple of macros for doing some stuff at compile-time (include constant data, and/or construct complex regexes from a higher-level description). The macro source code checks all AST references of the symbol as which the macro is imported. If the reference constitutes a function call (i.e. So, the source code Svelte gets to see imports a symbol from Now, Svelte generates new code that includes How about fixing it? So, it was easy to add a few lines of code to my macros that check if the macro symbol is used as a object property, and if so just replace it with So.... what to do? You could say that Svelte does not support that you use Babel macros, so you don't have to care about any of this. That would be a shame, because macros can be super useful. Of course, my modified macro still works, and I'll keep using it, but any future Svelte version might mangle the code in the One thing that might be helpful would be, if I could declare imports in my Svelte files in a way that tells Svelte not to bother about them. Or if there was a config option to the Svelte compiler to disable this whole Also, full disclosure, I am not a frontend/JavaScript expert, so I might not be seeing obvious solutions... or maybe I'm not even making sense... |
Did you actually tried it?
This is actually the case I had in mind with my proposed patch. $$self.$capture_state = () => ({ foobar });
// =>
$$self.$capture_state = () => ({ 6 }); // parse error
// here, the foobar on the left is NOT a reference to foobar in the AST
$$self.$capture_state = () => ({ foobar: foobar });
// =>
$$self.$capture_state = () => ({ foobar: 6 }); // OK
@tanhauhau Seemed to suggest that it was the expected correct behaviour for a macro. So I think it's probably an acceptable "workaround" for user defined macros. Now, I've proven to myself that there exists other published macros, like the
This might be an option actually. Long ago, I had a discussion with Rich H. about HMR, and he mentioned maybe there should be specific options for HMR instead of adding to dev mode. But this capture / inject mechanism is not used only by HMR. There is also dev tools (and maybe others I don't know about), so I don't know how this option would be called. There is also another, currently not implemented in Svelte, method that HMR would need eventually...
We can't add some special syntax just to handle this very edgy case but, all in all, I still think not capturing imports at all would be the right solution to all the problems identified here. HMR doesn't need them. I don't think they're useful for dev tools. And I can't think of another use case where they'd be useful. Anyway, once we're able to detect and exclude them, it would be easy to bring them back with a compile option; but, until the need is proven, I think it's better to avoid an extra compile option that would have to be documented, might confuse people, etc. |
@RedHatter You don't need to read all of the above, but do you have any opinion about excluding imported references from import foo from 'whatever' Would this be cool with dev tools? |
Thanks, @rixo! Sounds good to me. I'm just figuring these things out myself as we discuss this. Oh, just one thing about your patch: it doesn't make a difference to me. When my macro changes the code for the "ObjectProperty" the resulting code is changed from |
@rixo So sorry I took so long to reply. Yes, that would be fine. In fact, I think it would be preferable. |
Previously, these methods only applied to exported props. Also, add $$inject option to constructor, which injects state before running the update loop.
Make
$capture_state
able to capture all writable variables instead of just props.With this change HMR can use
$capture_state
to preserve local component state, instead of a custom implementation relying on compiler's output being past down by the bundler.I made capturing all state the default behaviour because it seems to me that it is consistent with the function's name. However it makes it a breaking change. This might be OK because, as far as I know, this function was intended specifically for HMR and so it probably has no user for now. Otherwise I can default
local
tofalse
to stay compatible with existing function, if needed.