logoalt Hacker News

Waterluvianlast Sunday at 9:03 PM5 repliesview on HN

I often imagine state and events as the two impulses that drive an application. I like React a lot, but a common pitfall is that it is 95% focused on state, and so you get odd cases where you end up trying to encode events as state.

You’ll see this anywhere you see a usePrevious-like hook that you then use to determine if something changed and act on it (eg. I hold state that a robot is offline, but I want to do something special when a robot goes offline). This is inferring an event from state.

I’ve had luck adding an event bus as a core driver of a complex react application for events I don’t want to track as state. But it always feels that it’s a bit in conflict with the state-driven nature of the application.


Replies

fleabitdevlast Sunday at 11:50 PM

Reactivity works by replaying code when its inputs have changed. Events can make this very expensive and impractical, because to properly replay event-driven code, you'd need to replay every event it's ever received.

When we replace an event stream with an observable variable, it's like a performance optimisation: "you can ignore all of the events which came before; here's an accumulated value which summarises the entire event stream". For example, a mouse movement event listener can often be reduced to an "is hovered" flag.

Serialising program state to plain data isn't always easy or convenient, but it's flexible enough. Reducing all events to state almost solves the problem of impure inputs to reactive functions.

Unfortunately, reactive functions usually have impure outputs, not just impure inputs. UI components might need to play a sound, write to a file, start an animation, perform an HTTP request, or notify a parent component that the "close" button has been clicked. It's really difficult to produce instantaneous side effects if you don't have instantaneous inputs to build on.

I can't see an obvious solution, but until we come up with one, reactive UI toolkits will continue to be ill-formed. For example, a React component <ClickCounter mouseButton> would be broken by default: clicks are delivered by events, so they're invisible to React, so the component will display an incorrect click count when the mouseButton prop changes.

RossBencinalast Monday at 3:31 AM

A phrase that I was expecting to see is "state transition." You want to be able to specify and execute actions on particular state transitions. In your case, you want to be able to emit events from these actions.

c-hendrickslast Sunday at 11:14 PM

There's a bit about effects vs events in React's own docs: https://react.dev/learn/you-might-not-need-an-effect

johnfnlast Sunday at 11:55 PM

I tend to be sus of usePrevious. Not saying it doesn't have a place, but often times it's cleaner to just write the diffing code you want directly in the handler. For instance, say you have a text field, and if the value changes you want to autosave. I would just put that right in the onBlur - `if (e.currentTarget.value != text) { autosave(e.currentTarget.value) }`. If you want to debounce, I would debounce the method, not the value.

I tend to agree with your overall assessment - your React code is not doing well if you're encoding events into state. That's why I try to avoid it! But I may be oversimplifying.

rtpglast Sunday at 11:08 PM

this is what reducers are for. Though reducers tend to make you end up needing to do a bunch of stuff on async event handling which can feel _pretty_ tedious. And if you don't do the tedious way, you often end up intro'ing really hard to debug issues.