swr
swr copied to clipboard
Subscription mode
To support subscription / disposable / observable data source, adding a new hook useSWRSubscription built on top of useSWR.
API
import type { Configuration } from 'swr'
import useSWRSupscription from 'swr/subscription'
useSWRSupscription(key, subscribe: SWRSubscription, config? Configuration)
export type SWRSubscription<Data = any, Error = any> = (
key: Key,
callback: (err?: Error, data?: Data) => void
) => void
export type SWRSubscriptionResponse<Data = any, Error = any> = {
data?: Data
error?: Error
}
export type SWRSubscriptionHook<Data = any, Error = any> = (
key: Key,
subscribe: SWRSubscription<Data, Error>,
config?: SWRConfiguration
) => SWRSubscriptionResponse<Data, Error>
the purpose is to provider users enough flexibility to manage their own subscription/disposable on user land, but still keep the argument shape different from function type fetcher.
Usage
const subscribe = (key, { next }) {
const dispose = remoteSource.subscribe(
(data) => next(undefined, data),
(err) => next(err)
)
return () => dispose()
}
const {data, error} = useSWRSubscription(key, subscribe)
This pull request is automatically built and testable in CodeSandbox.
To see build info of the built libraries, click here or the icon next to each commit SHA.
Latest deployment of this branch, based on commit 54dfaee951df9b59ed297e28f374a3390aad00bd:
Sandbox | Source |
---|---|
SWR-Basic | Configuration |
SWR-States | Configuration |
SWR-Infinite | Configuration |
SWR-SSR | Configuration |
Is this likely to get merged in soon? Subscriptions would be really useful for us
We're trying to get it into 1.1 or 1.2. But shortly we'd love to collect more use cases of subscription pattern in real world apps so that this API can be well abstract. If there're feedback from you would be very appreciated
Happy to help if I can. We're using subscriptions to have live data changes on multiple devices. We're using a graphql endpoint and currently have subscriptions set up like in this project. However, they don't handle the disposal of the websockets. I've been trying (and struggling) to implement my own disposal system using a similar hook to yours except it returns the dispose function with the data so that it can be called in the callback of a useEffect like:
const { data, error, dispose } = useSWRSubscribe(key, subscribeFn)
useEffect(() => {
return () => {
dispose()
}
}, [])
Thanks! Yeah we still have a couple of concerns:
- Should we leverage the new React useSyncExternalStore (useMutableSource) API?
- In subscription mode, many of the SWR configs will be unusable (suspense, polling, error retry, ...), what should we do?
- Should we provide APIs for manually pausing/stopping subscriptions?
Overall I think we can try to launch this in version 1.1/1.2 as an unstable API, and polish it overtime.
Should we leverage the new React useSyncExternalStore (useMutableSource) API?
At first glance this looks interesting and could be useful. I've not used it before though so would need to read deeper into it to understand it.
In subscription mode, many of the SWR configs will be unusable (suspense, polling, error retry, ...), what should we do?
I think the subscription mode would need its own set of config options. Some of those options wouldn't just be unusable but unnecessary too. I can't think of a reason why you would need to use polling with a subscription for example. For now it could just be a subset of the current SWR configs though it may need its own options adding in time.
Should we provide APIs for manually pausing/stopping subscriptions?
I could see this being useful though probably not necessary for an initial release.
I agree about launching this as an unstable API and polishing it over time.
Is this still going to be delivered?
Thank you for updating this! To push this forward, we need to set some minimal requirements to get this stable (under a flag or prefixed with unstable_). Here’re my thoughts:
- It needs to use the
useSyncExternalStore
API under the hood, to guarantee the stability and correctness when rendering the UI concurrently. (This can be added in a future update) - Would be great if we change
(onData, onError) => dispose
to also support(callback: (error, data) => void, ...keys) => dispose
, which follows the Node.js convention, but also passes the current keys. This aligns with theuseSWR
hook.
Hey! Kudos for your efforts! Hopefully I'm not creating extra pressure by asking when this update will be merged.
What's the best way to handle conditional subscription? For example key is undefined on mount.
const { data: token } = useToken(); // fetch token, async
const connection = useConnection(token); // connection will be undefined util token is fetched
const key= connection?.sid;
const { data: messages, mutate } = useSWRImmutable(key, () => connection.getMessages());
const subscribe = (key, { next }) => {
if (!connection) return; // we probably don't need this early return
const handler = (data) => {
const currentMessages = cache.get(key);
next(undefined, [...data, ...currentMessages]);
}
connection.on('messageAdd', handler);
return () => {
connection.off('messageAdd', handler);
};
}
useSWRSubscription(key, subscribe);
// pagination
const handleNextPage = () => mutate(async (currentMessages) => {
const nextPage = await currentMessages.getNextPage();
return [...nextPage, ...currentMessages]; // something like this
}, false);
key
is undefined on the first render, this will produce two entries in disposers
Map.
Something like this Map(2) {'' => ƒ, 'EXAMPLE_SID' => ƒ}
.
Is this intended behaviour?
I like the idea of this and i am currently trying to use SWR as a cache for WebSocket connections. In my use case I want to stash all incoming messages and use them as a log. With this current API I can only update the data with the incoming data. It would be nice if there was access to the current saved data through the next function to update the current data with old data and keep the cache. I don't know if this should be a use case for this hook and just wanted to give feedback. Thanks for the work on this.
I like the idea of this and i am currently trying to use SWR as a cache for WebSocket connections. In my use case I want to stash all incoming messages and use them as a log. With this current API I can only update the data with the incoming data. It would be nice if there was access to the current saved data through the next function to update the current data with old data and keep the cache. I don't know if this should be a use case for this hook and just wanted to give feedback. Thanks for the work on this.
@feledori you can always save some complex object in the cache instead of array, something like:
const { data, mutate } = useSWRImmutable(key, async () => {
const messages = await connection.getMessages();
return { messages, incoming:[] };
});
useSWRSubscription(key, (key, { next }) => {
const handler = (newMessage) => {
next(undefined, {
messages: [newMessage, ...data.messages],
incoming: [newMessage, ...data.incoming]
});
}
connection.on('messageAdd', handler);
return () => {
connection.off('messageAdd', handler);
};
});
console.log(data.messages);
console.log(data.incoming);
@huozhi hey! Any info on which milestone will include this, following 2.0?
Would love to see this merged! Is my assumption correct, that this can be used with grpc?