The useWebSocket
hook provides access to a client connection to the WebSocket
server, which is responsible for broadcasting events from the indexer. The
client is a singleton, meaing all hooks provide the same client. This
architecture removes the need for multiple connections or constant
re-connections when the page changes.
The useWebSocketChannel(s)
hooks wrap the web socket hook and return one or
several subscriptions to channels, keeping track of how many subscriptions there
are to each channel across the whole app. AppContextProvider
will unsubscribe
from a channel once all listeners have stopped listening to it.
The useOnWebSocketMessage
hook wraps the channel listener and provides a
simple interface to subscribe to messages across one or many channels matching
an expected type and calling a callback function when necessary.
The useOnCurrentDaoWebSocketMessage
hook is a shortcut for the above hook to
connect to the current DAO's channel. This can only be used when on a DAO's
page.
You most likely want to use useOnWebSocketMessage
or
useOnCurrentDaoWebSocketMessage
if you're adding WebSocket functionality. The
web socket and channel hooks manage the connection and subscriptions, so you
don't have to worry about those. useOnWebSocketMessage
is useful for
subscribing to multiple channels, such as the inbox that wants to be notified of
new proposals for all followed DAOs. useOnCurrentDaoWebSocketMessage
on the
other hand is useful for subscribing to the current DAO's channel, such as the
proposal page that wants to refresh when a vote is cast on the current DAO.
The useOnCurrentDaoWebSocketMessage
hook assumes the WebSocket always sends
messages under the broadcast
event name with a payload that contains type
(a
string) and data
(an object).
This hook accepts two required arguments and 1 optional argument:
expectedTypeOrTypes
(required, string or string array): The type or types of message you're expecting to receive.onMessage
(required, function): A callback function that will be called when a message is received if its containedtype
matches the expected type(s). It will be passed thedata
object from the message payload.defaultFallbackData
(optional, object): This only makes sense after the hook's response is explained, below. Keep reading.
This hook returns an object with two properties:
listening
(boolean): This istrue
if the hook is currently listening for messages. It'sfalse
if the hook is not listening for messages. This represents whether or not the WebSocket is connected. It should ideally betrue
most of the time, since the WebSocket should be connected for the entire time the app is open to a DAO page. If it'sfalse
, it likely means the user is not connected to the Internet, though the WebSocket server could have crashed. The WebSocket should attempt to reconnect automatically, but nothing is guaranteed.fallback
(function): Since the listener is not guaranteed to be listening, this function is provided that, when called, conditionally triggers theonMessage
callback. The conditions are described below. It accepts an optionaldata
object that will be passed to theonMessage
callback, and an optionaloptions
object to configure its behavior.- If
data
is not provided, thedefaultFallbackData
object passed to the hook will be used. If neither thedata
argument nordefaultFallbackData
hook argument is passed,fallback
callsonMessage
with an empty object. - The
options
object containsskipWait
andonlyIfNotListening
, which are both booleans. IfskipWait
istrue
,fallback
will trigger the callback immediately, not waiting for the next block. IfonlyIfNotListening
istrue
,fallback
will only trigger theonMessage
callback if the listener is not active (i.e.listening
isfalse
). This is especially useful if critical behavior relies on the listener receiving a message. For example, when casting a vote that needs to refresh and display the vote once cast, callingfallback
after the vote transaction with thedata
object that would have been received from the WebSocket ensures that callback will be triggered even if the listener is down. SinceskipWait
defaults tofalse
andonlyIfNotListening
defaults totrue
, the default behavior of this fallback function is to trigger the callback after waiting for the next block, only if the listener is not active.
- If
The onMessage
callback argument and fallback
function in the response are
both automatically memoized, so it's safe to pass callback functions without
wrapping them in a useCallback
to onMessage
, and it's also safe to pass
fallback
as a dependency to other hooks without causing unnecessary
re-renders.
useOnWebSocketMessage
is the same as the above hook, except it contains an
extra argument at the beginning for a list of channel names to apply the handler
to. Under the hood, useOnCurrentDaoWebSocketMessage
simply uses
useOnWebSocketMessage
with one channel—the current DAO.
Check out the DaoProposal
component to
see this hook in action. It's used to subscribe to the proposal
and vote
events to keep the UI up to date and display success alerts when the user's
vote, execution, and close actions finalize on-chain.
I tried to make it so that the fallback would wait to see if a message of the expected type arrived within the next block before deciding to trigger the callback; this would ensure that the callback would still be called if the WebSocket were listening OR if the WebSocket pretended to be listening but no event arrived after a small (1-2) block timeout. This would guarantee that even if the WebSocket were connected, but the indexer was down, the callback would still be called after a reasonable timeout.
However, it turns out that the indexer => WebSocket pipeline is so fast that
the desired message tends to arrive before the signing client's execute
(i.e. transaction sign/broadcast) Promise resolves. The fallback can only be
called after the Promise resolves since we have to wait for the wallet to
approve a transaction before waiting for its response. Thus, this fallback
implementation would often lead to duplicate callback executions, because the
fallback would be unable to detect the message in most cases.
This could be solved by storing the recent messages received, though this would create a lot of additional complexity; also, the same event could occur several times with no variation between messages—since, for example, some proposals let you cast revotes—so detecting if the event we expect to happen has already happened would be imperfect.
IMO the best solution is to make sure the indexer is functioning properly; worst case (rare) scenario is that the user gets stuck in a loading state after they perform some action, and they just have to refresh the page to fix it. This should only ever happen if the WebSocket is connected but the indexer is down.