3 ms·
I worked on a system where someone did this. They didn’t consider data races so managing this system became impossible. I don’t use redux, but I also don’t thi
by hermanradtke 3y ago
I worked on a system where someone did this. They didn’t consider data races so managing this system became impossible.
I don’t use redux, but I also don’t think this is the way.
- andrewstuart 3y ago>> They didn’t consider data races so managing this system became impossible. What do you mean? I build systems like this all the time and they work fine, no "data races" whatever that means.
- MrJohz 3y agoI 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): const ImageComponent = () => { return ( <div> <h2>Profile Image</h2> <img src={`https://example.com/profile-image.jpg?cachebust=${CACHE_BUST()}`} // Image URL with cacheBust as query param alt="Profile" style={{ maxWidth: '200px' }} /> </div> ); }; const DispatcherComponent = () => { return ( <div> <h2>Dispatcher Component</h2> <button onClick={reloadImage}>Reload Image</button> </div> ); }; 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.
- andrewstuart 3y ago>> 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.
- MrJohz 3y agoAs 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.
- andrewstuart 3y agoI do have some level of structure in that the messages are named and prefixed as GLOBAL or LOCAL useEffect(() => { document.addEventListener('GLOBAL_RELOAD_CHANNEL_INFO', loadChannelInfo) return () => document.removeEventListener('GLOBAL_RELOAD_CHANNEL_INFO', loadChannelInfo) }, [loadChannelInfo]) useEffect(() => { document.dispatchEvent(new CustomEvent('LOCAL_CLOSE_DIALOG_ALL')) }, []) useEffect(() => { const handler = () => setReloadCoverImage(Math.random().toString()) document.addEventListener('GLOBAL_RELOAD_RELOAD_COVER_IMAGE', handler) return () => document.removeEventListener('GLOBAL_RELOAD_RELOAD_COVER_IMAGE', handler) }, [setReloadCoverImage]) 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.