Swr: [RFC] Subscription mode for SWR

Created on 29 Oct 2020  路  6Comments  路  Source: vercel/swr

Define the Problem

currently useSWR and useSWRInfinite are more designed for endpoints or data sources that can directly access and patch proactively. The data flow is like below

useSWR <=== write and read ====> data source

But for some cases SWR doesn't have enough solid ability to interact with data sources such as observable data (states returned from an observable stream), subscribable data (data been sent from a remote websocket endpoint).

then the fetcher of SWR will lose meaning since they're not really accessible initiavely from user end. we can surly do a subscribe or observe call to attach on the source. however we cannot know the state of a certain time.

besides fetcher, the mutate, trigger and revalidate APIs are also kind f disabled due to the passive mode. I want to find a new way to think of the communication path.

Solution

propose to bring a new hook API useSWRObservable to SWR to fit in the subscribable situation. unlike the basic useSWR hook, it won't return the manipulation APIs of useSWR.

API

function useSWRObservable(key, observableFn: () => ObservableCloseHandler, conifg?): SWRObservableInterface;

type SWRObservableInterface = {
   data: any;
   error?: any;
}

function ObservableCloseHandler(): void // close the observation

Usage

// like the useSWRInfinite, we put it into an extra file
import {useSWRObservable} from 'swr' // or in the future we could make it import useSWRObservable from 'swr/observable'

function observeWebsocket(key, callbacks) {
   const ws = new WebSocket(key)
   ws.onmessage = (event) => { 
      callbacks.onSuccess(event.data)
   }
   ws.onerror = (error) => {
      callbacks.onError(event.data)   
   }
   return () => ws.close()
}

const {data, error, isConnecting} = useSWRObservable('wss://my.app/api/chat-messages', observeWebsocket)

then in this case, if the key changed, SWR is able to close and re-subscribe to the source for new key;
the uncertain thing is the returned error might not always be valid since some obseravle won't throw error when the connection get closed;

Other Possible Solutions

make revalidate and returned trigger with no effect. but mutate is still able to patch states to that key

Pros & Cons

Pros

  • support realtime data
  • adaptive to any observable data source, user can also do customization upon it
  • simple returned values to let user to leverage, no mind load on the meaning of revalidate/mutate/trigger

Cons

  • if there's any new API can access the current data, like geo API is subscribable but getting current position of geo is also doable. might need to have some workaround for that...
  • more APIs into SWR, might bring confusion to new users to choose API
RFC discussion

Most helpful comment

Thank you for writing this amazing RFC!

ws.onmessage = (event) => { return event.data };

This doesn't solve the data problem right? Like how do I notify SWR that I got the data (or error).

const {data, error} =

I think we can probably also return a connecting or loading value, which indicates the initial connection state (before the first value arrives).

or in the future we could make it import useSWRObservable from 'swr/observable'

I'm :+1: on doing it.

All 6 comments

Thank you for writing this amazing RFC!

ws.onmessage = (event) => { return event.data };

This doesn't solve the data problem right? Like how do I notify SWR that I got the data (or error).

const {data, error} =

I think we can probably also return a connecting or loading value, which indicates the initial connection state (before the first value arrives).

or in the future we could make it import useSWRObservable from 'swr/observable'

I'm :+1: on doing it.

Thanks for the quick feedback, I updated the draft, providing some callbacks as second args

- function observeWebsocket(key) {
+ function observeWebsocket(key, callbacks) {
   const ws = new WebSocket(key)
   ws.onmessage = (event) => {
-       return event.data
+       callbacks.onSuccess(event.data)

   }
+  ws.onerror = (error) => {
+     callbacks.onError(event.data)   
+  }
   return () => ws.close()
}

I think we can probably also return a connecting or loading value, which indicates the initial connection state (before the first value arrives).

I guess it could be isConnected or isSubscribed? since you can know ws connection is connecting/open/closing/closed, but for other observable stream, we might not know the short connecting/pending state before the connection is established. just my current opinion, and open to discussion : )

@huozhi or a general status value (which can be any) that can be passed? Since some subscriptions have multiple states (e.g.: there're 4 states of WS: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState).

Regards callbacks, I think we might need to adjust the argument order since we can have multiple arguments. How about:

useSWRObservable([1, 2, 3], (onData, onError, onStateUpdate, 1, 2, 3) => {
  // ...
})

Hmmm I think we still need to find something simpler...

yup, the basic event target like stuff and bservable sounds like 2 scenarios, the first one is more simple.

Event Target Like Subscriptions

// event target (web defined)
eventTarget.addEventListener/removeEventListener(fn)

// event emitter
eventEmitter.on/off(fn)

// subscribable
close = subscribable.subscribe(fn)
close()

Observable

subscription = observable.subscribe(onNext, onError, onComplete)

// or
subscription = observable.subscribe({
  next() {...},
  error() {...},
  complete() {...},
})

and obserable also has some interesting standard from community like callbag


so in that case, I feel we should either find a united way to let user encapsulate their observable to fit swr, or we make a very simple API contract with only few callbacks and let user handle the complexity themselves to keep swr lightweight.

just found use-subscription's API design for subscription case, to distinguish from observable case

Just come to mention that my PR (#457) come from a different use-case than the idea of the RFC.

The useSWRObservable hook is to get data from an observable source without when you don't have a non-observable source. This is useful when your source of data is only WebSockets or something real-time so you can't use a fetcher, it makes total sense here.

My proposal to add a subscribe in the config comes from a different idea where you have an API you can fetch with a fetcher but you want to start a subscription to an observable-source to receive updates without using long-polling.

Imagine this flow, in your fetcher you do a GET /api/resource/1 to get the resource with ID 1, then your subscribe to /ws//resource/1 in a WS server, this way you get an initial up-to-date data for your resource and thank to the subscription if something change in the server you don't need to wait for SWR to revalidate it, the API will let you know, and the reason to pass mutate is that sometimes this WS servers can return the new data, sometimes they return a delta of what changed so you will need to call mutate to apply the update, and sometimes they return that something changed and you will need to trigger a revalidation against the HTTP API.


So, I think we should support both, useSWRObservable and the subscribe option in useSWR.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

baoduy picture baoduy  路  4Comments

alexanderbluhm picture alexanderbluhm  路  3Comments

bbenezech picture bbenezech  路  5Comments

needcaffeine picture needcaffeine  路  3Comments

zahraHaghi picture zahraHaghi  路  3Comments