I also generally fall on the side of useMemo'ing everything pre-emptively, and this is a good argument for its effects on performance. However I think a more important effect is that preserving references between renders (and lack thereof) now has a direct effect on behavior due to useEffect's dependency list.
If a downstream component's useEffect depends on a prop that hasn't been useMemo'd, their effect will be executed on every render due to the changing reference, and there is nothing that can be done at the downstream component to alter that behavior. This means they'd have to trace the prop back to its source and refactor it to useMemo, which can be a very painful exercise as I'm sure anyone who has done it can attest to. For props that come from third party libraries, this might not even be an option, which is why pre-emptively useMemo'ing is even more crucial for reusable abstractions like shared hooks and components.
And yes, I realize that the React docs currently recommend using useMemo only as a performance optimization, not as a semantic guarantee, but I believe that ship has sailed. There is so much code in the wild today that useMemo and then pass the result to some useEffect downstream, that they can't really afford to break in practice. I think the only practical option at this point is to strengthen the original useMemo with a semantic guarantee, and then introduce a separate useMemoForPerformanceOnly (with a better name) hook that can be used to opt-out of the semantic guarantee to allow React to evict memoized results to optimize for memory usage.
I feel like you could go more into depth to defend your assertion "no memo is not an option." As someone who has spent hours on jsperf back in the day -- my apps feel blazing fast already. Many devs I know also don't use memo or useCallback at all. It sounds like you're adding an entire layer of extra abstraction "just in case." But then again, maybe your use case makes it warranted.
Interesting post, makes u wonder why the hell doesn't react do it by default. I'm really surprised to see that i have to think about performance in react apps so much, coming from elm i expected it to be all faster by default
Because Elm has language-level support, while hooks try to do things that should be language-level but aren't, which is why they have so many footguns in general.
To get concrete, there's no possible way this function could be automatically memoized at a library/framework level:
I wonder every day why React doesn't do a lot of things by default. Imagine specifying memoization dependencies by hand. Most of the stuff is named or put in the framework to raise engineering salaries for making CRUD applications or to make them feel smarter about doing basic web work.
React will go the way of JQuery in 5 years. We'll kindly thank it for it's contribution to the framework space and move on. No one in their right mind would pick it up for new apps over Vue, Svelte, Angular hell even Ember. Then you have the new wave of frameworks coming out. Only way that React is usable is only for the view layer with Mobx holding any other state.
This one is interesting, because React has a separation between the renderer and the library. There is a different renderer for e.g. react native, or even for the terminal (!!). It’s a little ambiguous where memoization falls (does it belong to the renderer, or to the core?). I totally agree that being able to switch on behavior for an entire tree or subtree would be ideal, but react tends to use components for that as well (e.g. <React.StrictMode>). Perhaps this should be a component, it could even be implemented via the context system.
The problem with using useMemo & useCallback everywhere is that the dependency arrays are really easy to get wrong. Small changes can easily lead to stale data or render loops. I'd much rather people take the time to consider each case. The React profiler is your friend here.
I think if you use them 100% of the time alongside eslint dependency arrays are really hard to get wrong. If you rarely use them, you always get them wrong.
If you just blindly follow the eslint suggestions you can very easily wind up with render loops. You need to carefully consider and understand the dependencies in each case. And if you want to do something specific when only one of your dependencies changes you need even more gymnastics.
If a downstream component's useEffect depends on a prop that hasn't been useMemo'd, their effect will be executed on every render due to the changing reference, and there is nothing that can be done at the downstream component to alter that behavior. This means they'd have to trace the prop back to its source and refactor it to useMemo, which can be a very painful exercise as I'm sure anyone who has done it can attest to. For props that come from third party libraries, this might not even be an option, which is why pre-emptively useMemo'ing is even more crucial for reusable abstractions like shared hooks and components.
And yes, I realize that the React docs currently recommend using useMemo only as a performance optimization, not as a semantic guarantee, but I believe that ship has sailed. There is so much code in the wild today that useMemo and then pass the result to some useEffect downstream, that they can't really afford to break in practice. I think the only practical option at this point is to strengthen the original useMemo with a semantic guarantee, and then introduce a separate useMemoForPerformanceOnly (with a better name) hook that can be used to opt-out of the semantic guarantee to allow React to evict memoized results to optimize for memory usage.