-
-
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
feat: pass update function to store setup callbacks #6750
feat: pass update function to store setup callbacks #6750
Conversation
This non-breaking change allows more complex store logic to be implemented, such as a derived store that accumulates a history of its parent store's values.
I've included API documentation, but haven't updated the tutorial or the examples. I figured this feature is a bit advanced for the tutorial, and none of the examples seemed like they'd benefit from showing off the new |
For the derived store a trivial (and no uncommon) example would be to keep a history. const store = writable(0);
const history = derived(store, (newValue, set, update) => {
update(values => [...values, newValue]);
}, []); But this makes me wonder, in the code you posted (copied here above) what would the initial value of |
The initial value in that example would be |
src/runtime/store/index.ts
Outdated
@@ -13,7 +13,7 @@ export type Updater<T> = (value: T) => T; | |||
type Invalidator<T> = (value?: T) => void; | |||
|
|||
/** Start and stop notification callbacks. */ | |||
export type StartStopNotifier<T> = (set: Subscriber<T>) => Unsubscriber | void; | |||
export type StartStopNotifier<T> = (set: Subscriber<T>, update?: (fn: Updater<T>) => void) => Unsubscriber | void; |
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.
This needs to be a non-optional parameter, else TS will error here if you try to use update
without checking for its existance first.
Which makes me think: Is this a breaking change for people implementing their own store contract? If so, yeah it needs to be an optional update
parameter. @Conduitry what's your stance on this?
If we leave it like this, this means we need to adjust the other signatures a bit to type them so that update
is not optional as it is in fact provided for readable
/writable
/derived
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.
Strange, the two unit tests I added ran just fine, and I'm not seeing any TS errors in VS code on my use of update
in the tests. I thought it would be a breaking change (at least from Typescript's perspective if not from Javascript's perspective) to make this non-optional as then everyone who has written readable(0, (set) => { });
would get Typescript complaining that they didn't take the second parameter. But then, I don't know all the subtleties of Typescript; maybe it's fine with omitting parameters to functions because Javascript allows that?
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.
BTW, at no point in the Svelte code does the update
function from StartStopNotifier get called; it's only passed to callbacks. And the callbacks can either take the update
parameter and use it, or not take it in which case it would obviously be an error for them to have a call to update(x => x+1)
. But I don't think making this optional will actually produce any Typescript errors, and I do think that making it non-optional will be a breaking change.
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.
For function parameters, the inverse is true: If the function expects a function-parameter with x arguments, you can pass in a function with zero to x parameters.
You don't see errors because the repository runs TS in non-strict mode (no strict null checks).
See this playground for more: https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABACwKYBt1wBTDALkWwCdUBHQgZymJjAHMAaROABygH4qa76BKRAF4AfIgBucGABMBAbwC+AKEVpMObAJGIFfANyIA9AcRg4iVsTgAjdKgC2KjFmwlyzNlE2jZixH5bsAHRQcAAycADuqMQAwgCGlKga+kaI0ZbEiFaoEHEgiQFQiBBwIOhSWaiI4FKowHSoUoryfEA
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 see. And https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABACwKYBt1wBTDALkWwCdUBHQgZymJjAHMAaROAByiprvoEpEBeAHyIAbnBgATPgG8AvgCh5aTDmx8hiOTwDciAPR7EYOIlbE4AI3SoAtkoxZsJcuuHT9huDZhRYDFuyIMJRGJmaW1jaIqD5oxPKIiYikZAB0UHAAMnAA7qjEAMIAhpSoarrysjzyQA shows that omitting update
will be no problem, so it won't break existing code. I'll make the change.
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.
It's a breaking change for everyone who uses StartStopNotifier
to implement their own store contract: https://www.typescriptlang.org/play?#code/C4TwDgpgBAysCGAnYcD2YByrgEsBmOEiUAvFABQDOEwAXFJcIjgHYDmANFAK5gAm8YBHqNm7AJSkAfFABuqHHwDcAKBV5uLAMa5ULKAFsQAeQDuLNIgjkAFhAA291PXgsQXRtzx56cJCmB0LFwCIkkAbxUoaIZgLzxyAHJUez5E8SUoAHosqDArWRxUbkp7ECgCFgguFlRTKCJEVERKKAAjCC14EuheASEoHFba+vgoKwBHbhwrPjykeAMaIhUAXyA
Was a little busy last week, but I've made the change to the StartStopNotifier type now. Now |
This will be a breaking change for anyone who uses the StartStopNotifier type in their custom stores.
2b638a5
to
a7e2a61
Compare
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 docs need to be updated (?
needs to removed from the function signatures), apart from that this looks good. Also great job on finding the potential repos where it would break for other people. The fact that there are several repos already using that type though makes me think it might be better to merge this in Svelte v4, not now. @Conduitry thoughts?
There are two kinds of breaking changes:
I'm much happier making changes of type 1 than of type 2. AFAICT, for anyone using Typescript, this would be a type-1 change, because the Typescript compiler would catch that their custom store is no longer fulfilling the StartStopNotifier type. However, anyone who's not using Typescript and is importing from one of those custom stores would find a runtime error when the I was hopeful when I saw #6737 (comment) that it might be possible to implement this in a non-breaking way, but now that I've thought about it, I'm afraid this is a breaking change after all. Because although it's not technically Svelte's responsibility to update your custom store's API if Svelte's stores API gains another feature, in reality there's an unwritten contract that custom stores will behave like Svelte stores, including in how they call callbacks. So although the callback API isn't documented in the official store contract (which only requires a I do really want to get this in, though, because Svelte 4 would be a long time to wait, and this would make many use cases quite a bit easier. |
@dummdidumm wrote:
Done in commit ef6e407. |
So the big thing stopping this for now is that if
B would expect to be able to use the Note that this change does not break current usage of custom stores but only future usage of said stores, so your code will not stop working overnight. |
This is one of the breaking changes, yes - although that is arguably the one I would actually be fine with. The (to me) bigger breaking change is that people using the store interfaces will get errors in their checks/IDE when using them. |
This a clear and obvious breaking change, there is no ambiguity. The question is, are we comfortable that this change offers enough value to be worth the upgrade pain for consumers. Strictly this should be v4 but we have shipped low impact breaking changes in minors before. One for the maintainers to discuss, but I wouldn’t expect much progress until we find the time to discuss that as efforts are mostly focused on kit at the moment. |
If this breaking change is considered for inclusion, I think it's worth considering #6777 as well, because that would also be a breaking change. Of course, #6777 might be rejected as "not enough value to be worth the extra cost", but if there's going to be a breaking change to the store API, might as well push both of those changes through at the same time to only pay the breaking-change price once. |
I've now created #6786 to implement #6777. Note that I created #6786 as a draft PR and that it includes the changes in this PR, because I believe this PR should be merged (or rejected) before #6786 is considered. If #6786 were merged first and this PR were merged second, then the If this PR is accepted, I'll immediately update #6786 to remove its draft status, and rebase it off of |
I found a live example of why this is needed. At 9:52 of this Youtube video, someone wrote the following code to update a Svelte import {readable, get} from 'svelte/store'
// ... Supabase setup and so on ...
const posts = readable(null, (set) => {
supabase
.from('posts')
.select('*')
.then(({error, data}) => set(data));
const subscription = supabase
.from('posts')
.on('*', (payload) => {
if (payload.eventType === 'INSERT') {
set([...get(posts), payload.new]);
}
})
.subscribe();
return () => supabase.removeSubscription(subscription);
}) Note how he wrote (I don't mean to pick on the author of that Youtube video, BTW; I simply happened to be watching it just now, noticed the inefficient use of |
This is doable as-is. function createStore() {
const {set, update, subscribe} = writable(null, (set)=>{
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => update(current => json))
// return () => supabase.removeSubscription(subscription);
})
return {subscribe}
} |
@chanced wrote:
For writeable stores, yes. But readable stores don't expose their Basically, I should have used Edit: Replied too fast. I just realized that what you did was create a readable store since you only expose its |
I've played with this for a few days now, creating different kinds of custom stores. Most are not ready for public eyeballs, but you can see some of my experiments at kwangure/storables. The current interface Writer<T> {
set(this: void, value: T): void;
update(this: void, updater: Updater<T>): void;
}
...
- type StartStopNotifier<T> = (set: Subscriber<T>) => Unsubscriber | void;
+ type StartStopNotifier<T> = (writer: Writer<T>) => Unsubscriber | void;
This kills two birds with one stone since custom stores can now also pass custom functionality by extending the My suggested change does not preclude merging this PR into Svelte 3, but I agree with @chanced that much of the cited use cases of the PR are already possible (albeit hacky and non-obvious). |
I think the solution @chanced offered is the best solution, but I would also like to offer an alternative syntax: i.e. const history = derived(store, (newValue, set) => {
...
}, []); or const history = derived(store, (newValue, { set, update }) => {
...
}, []); This allows for all the functionality and ergonomics in the suggested solution, but provides a clear path for any future extensibility and avoids any confusion about argument order. I see there are requests for other information to be provided to the derived callback (such as "what changed" arrays etc), so staying future proof seems advisable. Full disclosure: I already wrote a set of libraries with stores using this contract (https://github.com/WHenderson/stores-mono). |
We discussed this in the maintainer's meeting and decided against merging this before Svelte v4, since it's a breaking change for store implementers. |
Hi @dummdidumm, just wondering if my suggested syntax was discussed? |
@rmunn we've started a |
I think we first should decide if we want to change this at all |
@dummdidumm is attempting to deploy a commit to the Svelte Team on Vercel. A member of the Team first needs to authorize it. |
In case there aren't enough examples yet, another use case I have encountered where this would be handy is a websocket or SSE store that connects inside function create_sse_store() {
let event_source;
return readable([], (set, update) => {
event_source = new EventSource('/sse');
event_source.addEventListener('message', (event) => {
update((messages) => [...messages, JSON.parse(event.data)]);
});
return () => event_source.close();
});
} |
@benmccann Sorry I didn't see the notification last month (Apr 12); that was a busy time for me and I wasn't checking my email often enough. Thanks @dummdidumm for tackling the rebase and getting this merged into Svelte 4! |
This
non-breaking changemostly non-breaking change (which is a breaking change for custom stores, see below) allows more complex store logic to be implemented, such as a derived store that accumulates a history of its parent store's values. The setup callback inreadable
andwritable
stores can now be either in(set) => { }
form or in(set, update) => { }
form. And the value-deriving callback inderived
can now take an optional third parameterupdate
in addition to the optional second parameterset
.Most of the use cases that would benefit from this are already possible, but would be made easier with this PR. For example:
Instead of:
Fixes #4880.
Fixes #6737.
Breaking changes for custom stores
This is a non-breaking change for anyone using Svelte stores, as you can continue to write
(set) => { ... }
as well as(set, update) => { ... }
in your stores'start
callbacks. However, this is a breaking change for any code that creates a custom store. Your custom stores will now need to pass anupdate
parameter to thestart
callback, as well as aset
parameter. Most of the time it will be as simple as this:Before submitting the PR, please make sure you do the following
[feat]
,[fix]
,[chore]
, or[docs]
.Tests
npm test
and lint the project withnpm run lint