Code examples are mostly to demonstrate the abilities, rather than real use cases
Reactivity
Let’s begin from the core concept React relies on – reactivity. Reactivity means that a system automatically updates in response on data flow change. There are just two things which represent data flow in react:
- props
- state
Or simply external and internal states. When any of these changes, React triggers component re-render:
Loading code example...
Back in the days react had an additional option to force re-render of a class-based component with
forceUpdate method. But for functional components React no more provides similar functionality. If we want to emulate a forced update, we can write a custom hook like this:Loading code example...
Personally, I really glad that we no more have native way to force an update (for functional components), because it aligns better with the data-flow philosophy, where real changes dictate when the update should happen.
Reactivity is a well-known pattern which is used and recognized by developers who writes and reads react code daily.
Hooks
Speaking of hooks, they rely on reactivity model as well: when their dependencies change, hooks get re-executed:
Loading code example...
Such design allows to distinguish actual state from the stale one. However, there is an interesting hook, which allows us to ignore reactivity, but at the same time it provides us with the up to date value at the moment of reading it. Meet
useRef:Loading code example...
Despite it's not necessary to list
ref as a dependency, I did it to show that even if you list it, it will not break or prohibit anything, because this is how reactivity works in that case: ref is always the same object which reference is stable, and what really changes is its current property. But React only checks for changes of values itself, listed within dependencies. And if there are no changes, the hook will not be re-executed. That’s why it’s absolutely safe to list or not to list value returned by useRef as a dependency.However, there is actually a case when we do have to list
useRef's value as a dependency – when it’s returned from a custom hook:Loading code example...
Here we have to list the
ref as a dependency to satisfy eslint rules, because it doesn’t know what is returned by useCustomRef hook. For eslint the ref is just an internal state. But thanks to the nature of reactivity, even if we list it, the effect will behave the same way as if we don’t list it 😅Even having such a non-reactive hook, in general, lets React handle all the hooks under the same set of rules, and it’s totally fine. On top of that, we are allowed to write custom hooks, combining and returning them in any allowed way.
And the most important thing – we doidn’t have rules, which are only for specific hooks.
ESLint
As was mentioned above, eslint is something which has to be satisfied in order to make React apps work properly. Without eslint reports it’s easy to miss a dependency, violate the rules of hooks, etc. The issue behind eslint rules is that most of them are not really the rules – they are agreements, which were easier to move to a linter stage instead of designing framework around them (that’s my guess). For example it’s totally fine to name your hooks without “use” at the beginning. They will still work. The only real sense behind this requirement is to allow eslint to detect the call order of your custom hooks, and prevent calling them not from a top level (which is also technically fine in some cases) – without the “use” prefix eslint would never know if it analyzes just a function or a hook.
So I just want to say, that eslint in React world is not an option, it’s a must.
useEffectEvent
React 19 introduced us with a new hook –
useEffectEvent. I won’t cover the cases it’s intended for, but I’d like to review its design.When I first read about it, I thought: “cool, now we have a way to declare a non-reactive callback, which is able to read the actual component’s state”. I was judging just by API: “If we don’t need to list the value returned by
useEffectEvent within the effect dependencies, then it probably holds a stable reference”. It would basically mean, that from this point React is no longer obviously reactive, which a bit weird 🤔But diving into the docs made me realise how wrong I was. As you might guess, the reference returned from
useEffectEvent is… not stable. It means, that if you accidentally list it within effect’s dependencies, your app will probably end up in infinite render loop:Loading code example...
Can you see the issue? It’s the first time React introduces an unforgiving mechanism, which goes against well-established reactive patterns for hooks. Compare with how elegantly
useRef works in this case – it won’t punish you if you list it as a dependency:Loading code example...
Now let’s consider the earlier mentioned case, but for
useEffectEvent returned from a custom hook.Loading code example...
If we judge only from our previous knowledge, I would guess, that eslint will ask me to list
sendLog as a dependency, since for eslint it’s just an internal dependency. But we also know, that the value returned from useEffectEvent should not be listed as a dependency. So what do we do? The answer is much more dramatical and not that direct. At first, before React 19, the
eslint-plugin-react-hooks allowed us to write code like that, which in a simple manner emulates useEffectEvent behavior:Loading code example...
This example demonstrates, how can we make an effect which updates only when
id changes and at the same time it always has access to the latest value of prefix prop. This would not be possible with useCallback:Loading code example...
because in this case
sendLog will be called every time id AND prefix changes. Why prefix, despite it’s not listed in effect dependencies? Because changes in prefix will trigger the useCallback to return a new value to sendLog variable, which will cause update to effect, because sendLog is listed as a dependency 🙃 I know, it’s a bit confusing, but that how we struggled before React 19.The first major change React team made to introduce
useEffectEvent was reducing the “power” of useRef. The latest eslint-plugin-react-hooks rules now throw an error (not even a warning) when you try to access .current property during render:Loading code example...
This way React prepares us to switch to
useEffectEvent and avoid hacks we used before. Honestly, that’s already doubtful, cause this change brings nothing but an attempt to force people write code by following eslint rules, and not by the rules React also accepts.
In fact it’s a breaking change, lol
As well as now you can’t pass referenced value down to component props:
Loading code example...
All of that was possible before
useEffectEvent introduction.Okay, let me remind you, what we are doing. We continue guessing how React now reacts to the code like that, when we try to share the usage of
useEffectEvent:Loading code example...
In short, we can’t share the logic handled by
useEffectEvent. As Dan Abramov explained in one of the issues, the mental model behind this decision is that effect event directly connected to the effect it was created for. Thus, it should be defined and used right at the place where the effect itself is defined. And this is the second major change the react team introduced to React 19: now eslint rules dictate how should we write our custom hooks. And for example if I have a code which relies on context and input parameters, and I want to reuse it, I simply can’t!Let’s consider such example:
Loading code example...
This is quite a realistic custom hook that I would want to share and reuse. However, eslint will complain with error:
React Hook "useEffectEvent" can only be called at the top level of your component. It cannot be passed down.
And the reason behind this prohibition is… eslint, again! Not an abstraction you wrote about, Dan. Why I say that this is because of eslint? Just remember the example we already reviewed for
useRef:Loading code example...
When something is returned from a hook and it’s used inside an effect, eslint will strongly insist to include this variable as a dependency. For the refs it works nicely, because of their stable references. But the reference returned by
useEffectEvent is not stable, which will trigger unwanted effect updates 🤷♂️That is the real reason, and seems like a architectural limitation of React at this point.
And the 3rd major change I see here is that now we have a hook, which is basically can only be used in pair with
useEffect , it’s even stated it in its name: useEffectEvent.Fake hook or not?
Just try to realize, what’s happened in order to introduce
useEffectEvent :- ESLint now throws errors by default, not just warnings, effectively making it a built-in part of the library, where React team moved functionality they couldn’t (or chose not to) implement directly in React itself
- The behavior of
useEffectEventgoes against developer habits in how hooks are usually written and structured. Its introduction also required breaking changes in howuseRefis used
- The name
useEffectEventdoesn’t sound like a standalone hook, but more like an extension ofuseEffect. And in fact, it doesn’t make much sense withoutuseEffect. This raises the question: why is it a hook at all, rather than a helper function or an option?
Overall
useEffectEvent is a great addition, which solves some painful issues we had before. From the perspective of the rules of hooks it is a new hook, I agree. But from the side of developer experience, it’s an engineering Frankenstein, which doesn’t follow any patterns developers used to use before. I just hope it’s not the start of a new era of the hooks, where every of them behave on its own, but kind of necessary transitional step to even more optimizations that can be moved to React’s compiler..png?table=block&id=2503641a-bc59-805e-a747-d35c67f5f7f0&cache=v2)
.png?table=block&id=2a73641a-bc59-80d1-9ca9-f93dca727950&cache=v2)