I love React but I use no state management, apart from useState locally within components.
State management in React is a major source of pain and complexity and if you build you application using events then you can eliminate state entirely.
Most state management in react is used to fill out props and get the application to behave in a certain way - don't do it - too hard, drop all that.
Here is how to control your React application and make it simple and get rid of all state except useState:
Trust me - once you switch to using custom events you can ditch all that crazy state and crazy usage of props to drive the behaviour of your application.
I also use native events in a number of places, and it can work well if you're very strict and judicious with your events, but the danger is always that one ends up with a global event bus with none of the observability or structure that make effect buses work well.
Data races are a specific example of something that happens when you don't have that observability, where the flow of events through the system produces behaviour that's dependent more on event timing than anything predictable. For example, you could have an event that triggers multiple asynchronous processes in different parts of the codebase, that in turn both access or update the same bit of state. This means that the final result of the state will be dependent on the order in which the state accesses/mutations happened. Importantly, with event buses like this, particularly ones built in a more ad-hoc way, it can be very difficult to notice when this happens without keeping the whole application (and all its possible events and responses) in your head at once.
In my experience, most of the complexity from frontend development comes from managing changing state, and one of the key problems is understanding why a given bit of state is the way that it is. I want to be able to easily trace what caused a change, where it was triggered, why it occurred, etc. But event buses can become messy because handling and dispatching a given event can happen anywhere. So even if you're sure there's no data races now, the longer the application exists and the more people work on it, the harder it becomes to ensure that data races aren't happening somewhere that you've not thought about.
That said, I agree that other state management tools that try and solve this can produce a lot of their own complexity. Redux, at least raw and with all the boilerplate, can be painful to use.
Personally, I've found signals to be one of the more convenient mechanisms for this kind of global interaction. Signals are similar to events in that they can be emitted and listened to, but they are more explicitly about the flow of data rather than arbitrary events fitting around the place. This makes it easier to hook into what's going on and see how the data is being changed. There are signals libraries for React (MobX is the classic one here), but these days I mostly just use SolidJS, which is a framework that looks a bit like React, but uses signals as its core reactive primitive.
I'm on my phone, so I can't give great code samples, but with events I'd probably write a separate class/service/tool to handle the cache busting state like this:
createCacheBust() {
// cacheBust here is a reactive getter, not just a value
// Calling `cacheBust()` in the right place will ensure that the
// effect or component will be rerun whenever
// `cacheBust` changes.
cost [cacheBust, setCacheBust] = createSignal(Date.now());
function reloadImage() {
// Update the state, trigger rerenders in any component that used `cacheBust()`
setCacheBust(Date.now());
}
return [cacheBust, reloadImage] as const;
}
export const [CACHE_BUST, reloadImage] = createCacheBust();
In simple cases like this, you can just call this "hook" globally, although in more complicated situations you might prefer using contexts to pass these values around.
In components you can just import these values and call them (or pull them out of a context and call them, if that takes your fancy):
You might say that this doesn't look much different to the event bus version, and that's true, but the benefit is that we've encapsulated the state somewhat. Before, emitting an event could do pretty much anything, and the state it affected was spread out over any component that chose to listen to the event. Now, the state lives in one place, and it is mediated by the exported API. If the state starts behaving unexpectedly we know immediately where we can start our journey to figure out what's going on: the createCacheBust function.
This is just how things might look in SolidJS, but you can also use Preact which merges the React and signal-based worlds in a different way. But either way, I really recommend exploring signals as a way of being able to wrap the event-based logic that you're currently using in a simpler, more state-based parcel.
>> you might prefer using contexts to pass these values around
I'm not getting the thrust of this.
So instead of simple, easy to understand messages being sent around we are now talking abstracted functions contained in contexts to send event messages? Sounds unnecessarily complex. May as well implement Redux.
As I said, you don't need contexts for this, signals can be completely global, and in the example I gave the signals were global (you don't even really need the function).
But the whole point is that "simple, easy to understand" is very context-dependent. Goto, for example, is very simple, possibly the simplest form of control flow you can imagine, and it's also easy to understand at the point that you're writing it down. It just moves control flow to where you want it to go - what could be simpler than that?
But simplicity needs at least some structure, otherwise it becomes chaos. That's the value of encapsulating both the events and the state in a single place, and then providing a limited interface to that reactive state. Yes, it's (slightly) more up-front work to write that interface out explicitly, as opposed to just putting things into events, but it has real value later on when someone else needs to read the code and understand what it's going to do.
However when programming React I try to avoid even layers of abstraction beyond what is already enough complexity.
The argument against events is losing track of where they came from and what they do but honestly I found that using state to drive the application UI was vastly more complex and hard to manage and debug than event messages. It's easy to do a global search for GLOBAL_RELOAD_RELOAD_COVER_IMAGE to see what a message does than to trawl your way throw the harrowing complexity and intertwined complexity of state, props, renders and re-renders.
And when I can, I attach the events to local elements in the component rather than document, to further reduce the scope of the complexity.
Why not use a MobX state class, then just write to state.cache_date? The state is in the parent lexical scope of the components, so no prop drilling or providers.
I used to use events just like this, but once the app gets bigger it’s hard to keep track as the event keys are strings. There is no IDE support for tracking usage (functions and class properties allow jumping to and from usage).
Also MobX tracks dependencies based on reads and auto re-renders changed components. This replaces having to hook up event receivers manually to re-render.
I think there are newer mobx state management systems nowadays (Signals is the new term).
Events and state need not be mutually exclusive. I like Vue’s approach of state going down the DOM tree and events going up. This works really well with Context and custom events that are dispatched by components (vs by the window as you did).
IMO global events can get messy quickly and make it really hard to develop more complex apps where you have the same component in multiple branches of the DOM firing the same events.
This is similar to one method that I’ve used in the past with the BroadcastChannel API. You can essentially create a pub-sub pattern in the frontend. It worked well for microFEs, but now that Preact released their Signals library I’ve moved to that.
Building on top components (un)mounting makes your components impure (it called useEffect for a reason). Suddenly you can't trust what you see because THAT might be the edge case. Unit tests don't work either.
I don't see anything in common with Redux or contexts.
No libraries. No higher order components. No concepts to learn. No weird rules. No complex tracing of props and trying to work out why state updates were not triggered, or were triggered.
Redux is one of the most painful software development experiences I have had.
Just the thought of using Redux fills me with dread.
The event code above simply sends a message directly between components - that's nothing at all like Redux.
the concept is that events subvert React's own change detection. it's definitely more direct than stacking contexts on top of each other (... or Redux, shivers)
but probably more maintainable, after all you can just search for the event name.
I just don’t understand why you’d like react if you like working with events. Sounds like you just JSX? Wouldn’t you get better served by something like lit?
I think react is great except the concept of using a one way flow of props to drive the application via state is flawed and too complex and hard to debug and has cross cutting issues.
React provides a nice application architecture with minimal usage of props and no global state or contexts.
State management in React is a major source of pain and complexity and if you build you application using events then you can eliminate state entirely.
Most state management in react is used to fill out props and get the application to behave in a certain way - don't do it - too hard, drop all that.
Here is how to control your React application and make it simple and get rid of all state except useState:
Trust me - once you switch to using custom events you can ditch all that crazy state and crazy usage of props to drive the behaviour of your application.