Implementing serverless chat in React
When developing simple client-side React applications, one often needs a touch of backend services. Not an entire database (with its management & administration needs) or high performance compute. These services have their own use cases, but for simpler requirements, they often take more energy to set up than necessary. It’s also difficult to scale these services up or down depending on usage, which might lead to unnecessary overhead (when you don’t utilize them fully) or service interruptions (due to traffic spikes).
Enters the notion of serverless infrastructure — a paradigm of providing backend services to the front-end on a as-used basis. Serverless vendors charge usage, not bandwidth and hence solve two problems with one solution — that of boosting (cost) efficiency and scaling up on demand. This creates a refuge where application developers can write and deploy code without worrying about infrastructure needs. Like all refuges however, there are trade-offs in inhabiting this system. For the use-case detailed in this post, serverless computing suffices and I’ll outline how I used Cloudflare’s edge network as a serverless platform.
Cloudflare workers are JS modules / functions running on Cloudflare's global network in over 200 cities around the world. Cloudflare's reputation of providing DDOS protection services implies that they have spent time optimizing resilient cloud networks and CDN services, and they offer to front web traffic for other businesses. So in essence, a cloudflare worker is a reliable online JS function which is run on CF's network servers anytime you call the function. This is great for non-persistent (light) compute, but the workers platform also includes simplified key-value storage and consistent distributed storage via Durable Objects. Of interest to us is this last one, namely Durable Objects. We want to implement a chat application (with it's UI layer written in React) over a global distributed network.
Suppose there are five participants in different parts of the world, talking in a group chat. To provide real-time feedback, one needs low-latency coordination among the servers closest to these users and even more importantly, the messages in the group chat have to be in one order for all users (a fairly difficult problem). See how quickly the problem of enforcing consistency over distributed systems becomes demanding? Cloudflare's Durable Objects try to tackle this by implementing a locking mechanism to enforce Global Uniqueness and using a transactional single-threaded storage API while reading / modifying key-value pairs. This suffices for a wide variety of use-cases (certainly ours in this blog post), but you can also think of use-cases where you might need more local (& better) consistency resolution (like in one particular geographical region).
Our worker implements a
fetch listener (wrapped in an error handler) and passes on API requests further, to instantiate a ChatRoom class. The instance of this class runs as a Durable Object and creates chatrooms while handling sessions to this chatroom. Since we don't want to persist data, we don't store it anywhere and all data is lost as soon as the websockets enabling the sessions are disconnected / closed. The ChatRoom Durable Object also instantiates a RateLimiter class to ensure worker compute is not abused by heavy traffic, but is rather moderated via rate limiting.
Passing callback functions to React's
One of the issues I came across while implementing the UI in React was to re-render the components upon receiving messages through the web socket. React components usually do not re-render until some state (which is being accessed in the component) is updated. This is, in general, a strength of React as it promotes performant rendering while defering expensive operations -- namely, re-render only when necessary. So when sometimes there is no explicit need of state in a component, external triggers of rendering are carried out by dummy state, like in this instance, a dummy
msgCount state number.
Another issue I faced was sometimes encountering incoherent state updates (and renders) in my App component. When using the
useState hook, the setter function (some form of
setState) is used to update local state and ask React to re-render the component (update the DOM). Usually, one often just calls
setState with a new value passed in as an object, for e.g.,
setState(newState). What if our
newState depends on the current
state variable? Directly accessing
state is not a reliable way to cause this update as React handles state updates asynchronously. This doesn't show easily in all applications (for instance, when some other state variable being updated pushes this state variable to also be updated and saves our application from breaking) or when the state update is a rather isolated low-cost operation (which somehow escaped React's batching of operations for the next render). To quote React's documentation, "Because
this.state may be updated asynchronously, you should not rely on their values for calculating the next state." Now I usually don't face these failed "renders", but in this app, frequent websocket messages (which often happen in fast succession) "failed to re-render the UI" and any further UI updates almost "missed out" the state updates before (for e.g., if you're appending a
messages list with messages flowing from a websocket). At first, I tried to fix this by adding more dummy state variables (without success) and using
useRef hooks to force more re-renders, before finally realizing the main problem. What I wanted was not more renders, but more reliable state updates, as the excess renders also kept showing the old state, although sometimes incoherently. It wasn't the renders that were failing, but the state updates.
The conclusion is to pass callback functions to
setState hooks when one needs the current state or props to calculate the next state. React is then able to reliably queue up these "pipes" / transformations to apply to the current state, without relying on what the current state actually is. This is almost so important that there should be an ESLint plugin for this (and there is one).
This app has been pretty useful to me to acknowledge the subtleties of React's
useState hook (and rendering mechanism) and to understand how capable serverless infrastructure is. Try out the app, and let me know your thoughts!