swr icon indicating copy to clipboard operation
swr copied to clipboard

Subscription mode

Open huozhi opened this issue 2 years ago • 12 comments

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)

huozhi avatar Jul 02 '21 04:07 huozhi

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

codesandbox-ci[bot] avatar Jul 02 '21 04:07 codesandbox-ci[bot]

Is this likely to get merged in soon? Subscriptions would be really useful for us

jamesalester avatar Sep 06 '21 13:09 jamesalester

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

huozhi avatar Sep 06 '21 14:09 huozhi

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()
  }
}, [])

jamesalester avatar Sep 06 '21 15:09 jamesalester

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.

shuding avatar Sep 06 '21 15:09 shuding

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.

jamesalester avatar Sep 06 '21 16:09 jamesalester

Is this still going to be delivered?

avisra avatar Jan 26 '22 13:01 avisra

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 the useSWR hook.

shuding avatar Feb 02 '22 01:02 shuding

Hey! Kudos for your efforts!  Hopefully I'm not creating extra pressure by asking when this update will be merged. 

MistreanuIonutCosmin avatar May 17 '22 14:05 MistreanuIonutCosmin

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?

isBatak avatar May 19 '22 14:05 isBatak

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 avatar Jul 12 '22 11:07 feledori

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);

isBatak avatar Jul 13 '22 07:07 isBatak

@huozhi hey! Any info on which milestone will include this, following 2.0?

mike-shtil-loop avatar Jan 23 '23 09:01 mike-shtil-loop

Would love to see this merged! Is my assumption correct, that this can be used with grpc?

karlo-humanmanaged avatar Feb 17 '23 09:02 karlo-humanmanaged