β€’

tech

Events: the most accurate representation of state

If you are looking for the one solution for your state management problems, the answer is events. Testable, reusable, composable state, all achieved from a rather simple abstraction.


Sandro Maglione

Sandro Maglione

Software

Events solve state management πŸš€

How is that something as simple as an event can achieve such wonder?

Here are some reasons and use cases for you πŸ‘‡


Single entry point

The key (for me) of state management with events is separation of concerns.

Aka, logic is separate from UI ✨

useState in React is all inside the component. State, actions (functions), implicit events. All of this manual and mostly imperative:

Events act as a gateway between UI and state logic: UI -> Read state + Send events, how the state changes is an "outside" problem.

Therefore:

  • Independent UI (you can implement it in parallel and separate from the logic)
  • Reusable logic (not specific to any UI)
  • Testable logic, without any rendering

All enabled by a simple abstraction like events πŸ’πŸΌβ€β™‚οΈ

Events as pure functions

The "state manager" receives events and changes the state. That's it (at a high level).

In other words: it's a pure function. Events as input, state as output.

And pure functions are ideal for testing, composability, and local reasoning 🀝

From setValue(value) to send({ type: "update", value }):

// Before
const onUpdate = (value: string) => { setValue(value); }
const onReset = () => { /** ... */ }
const onSubmit = (formData: FormData) => { /** ... */ }

// After
type Events = 
  | { type: "update", value: string }
  | { type: "reset" }
  | { type: "submit", formData: FormData };

const send = (input: Events) => { /** ... */ }

A single pure send function instead of multiple functions and direct updates.

"Pure" is the ideal. In practice, you want some way to handle effects.

In XState for example you use actors πŸ‘€

History with events

Historical context is required for debugging, auditing, or reconstructing past states (undo/redo).

Events have a "single" entry point (i.e. send) and provide all the information needed to perform an action.

Therefore, you can store a log of them to implement an history ✨

With setValue(value) you have no way of going back or understanding what happened (the previous value is replaced). Events solve this πŸ’πŸΌβ€β™‚οΈ

Sync

A sync engine aims to converge distributed data to a shared consistent state.

Events unlock this. You have a log for each client, and a sync engine "just" defines how to merge these logs:

type Log = Events[];
const syncEngine = (log1: Log, log2: Log): Log => { /** πŸͺ„ */ }

Log has all the details of everything that happened. As long as the syncEngine function converges to the same state for all clients, you have your syncing 🀝

More power, more code

With all these benefits, why not events always and everywhere?

Answer: no reason. Go with events! 🫑

Yes, but with software the correct answer is always "it depends".

Here are some valid reasons when to not use events:

  • Performance: direct updates are faster than passing from events
  • Reasoning: working with data directly in simple cases avoids the overhead of dealing with events
  • Verbosity: setting up events requires more code to read and understand
  • Less abstraction: events add another layer of abstraction, not always necessary for simple cases

I am publishing more and more daily content on X on everything effect, xstate, and TypeScript:

Next week I fly to Berlin for localfirstconf. Full report coming next week πŸ”œ

See you next πŸ‘‹

Start here.

Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights

Not convinced? Well, let me tell you more about it