I've been working on a large webapp for some time now and there was this one reoccurring problem that I just could not find a satisfactory answer to. The problem of how to make the user feel like she is not working on a mere stale copy of the real thing but on something that is alive and responsive. Taking away the async client/server feeling, giving the feeling of true sync collaboration.
TL/DR
I've created a small react library that offers a hook that you can use to subscribe to server side events and do something (probably some refetching) when they occur. I've called it stups (german for nudge or push).
https://github.com/cfs-gmbh/stups
The problem
Since I had worked with firebase firestore on the project before my current one, I was just used to seamless realtime updates and a UX that did not rely on manual refreshes or polling, but on immediate server side initiated updates.
We had decided to go with GraphQL for several reasons I don't want to dive into. We had also decided to go with apollo federation and apollo client. The former meant that GraphQL subscriptions where no option for realtime. Polling neither was since it really doesn't scale well and also doesn't give a good user experience for chats or realtime collaboration. At least if you poll in sane intervals.
Well, guess that's enough chit-chat. You now know the why, lets proceed to the what and how.
C'mon do some refetching
There are many different approaches of handling data fetching on the client-side and some of them come with a lot of advanced techniques like caching or pagination support implemented really well. You're right, I'm especially talking about tools like React Query, Apollo Client, urql and SWR here. So I had really no intention to reinvent the wheel and implement all this stuff on my own.
Many advanced data fetching tools include strategies to keep client-side data up to date. Fetching data when the window comes in focus and traditional polling are the common ones here. In some cases this might be enough. In others not.
One great feature, most of the mentioned tools share is that they offer some trigger to refetch the cached data. So now the only thing we need to do, is to call the refetch trigger if something has changed. This way, we can keep all advantages of our fetching library and add realtime push updates.
A hook to do anything
Stups (german for nudge or push) is not opinionated in what you can use it for. The above scenario is only the scenario that is useful for me, but you can use it to trigger any action you want to trigger.
The context provider of stups creates and keeps the websocket connection, while the hook allows you to register callbacks on certain events. The events are represented as strings in the form eventname:id
. You can also use *
as a wildcard for id. The callback function can receive the id as a parameter. I use use the entity name of what has changed on the server as eventname
.
Getting started
Install stups
yarn add stups
or
npm i stups
Wrap all your components that need to use stups in a <SubscriptionProvider>
.
const App = () => {
return (
<SubscriptionsProvider endpointUrl="ws://localhost:8080/" token="someJWT">
<Home></Home>
</SubscriptionsProvider>
);
};
The token attibute needs to carry a JWT in the form Bearer JWT which is sent to the endpoint, so the client can be authenticated and identified.
Now the useStups hook can be used in your components.
export function Home() {
const doSomething = React.useCallback(
eventId => console.log(`Do something for event ${eventId}`),
[]
);
useStups('event:*', doSomething, 'home');
return <div>Look at the console!</div>;
}
Checkout https://github.com/cfs-gmbh/stups/blob/main/example/server/server.ts
for a basic server implementation that offers an http endpoint to accept push events from your services.
About websockets
Most react devs, me included, think that hooks are quite fancy. They give us a lightweight mechanism to handle side effects, live cycle events, state management and so on.
Websockets do not fit this approach very well. They are a browser API and once there is a websocket connections, it should be handled like some kind of global state. This is what stups does for you.
The good thing about websockets is that they are well supported in all modern browsers, the bad thing is that they are a bit clunky to use. They are also not very opinionated in their purpose. We mainly use the server-to-client communication of websockets, because this is what we need to notify the client.
I see stups mainly as a wrapper for an opinionated use case of websockets. Since websockets are part of the browser, stups is super small (< 4Kb).
About the future
We're already using this solution in production at app.ava.services. But there are many improvements that need to be done.
The first step I see as necessary is that there should be a reference implementation for the server side, that offers a docker image, as well as a more generalized way to handle user identification.
Another big improvement would be to add support for http/2 streaming and http chunked encoding as fallback and/or upgrade mechanisms.
Another very important thing to add in the future are tests to ensure quality. Since we only have very limited resources, we have not been able to accomplish this yet.
Thanks for reading, I'm happy for feedback. Please follow me on twitter https://twitter.com/DavidAMaier!
Top comments (0)