> Adding non-primitive props you get passed into your component to internal dependency arrays is rarely right, because this component has no control over the referential stability of those props.
I think this is wrong? If you memoize a callback with useCallback and that callback uses something from props without putting it in the dependency array, and then the props change & the callback runs, the callback will use the original/stale value from the props and that's almost certainly a bug.
They might be trying to say you just shouldn't use useCallback in that situation, but at best it's very confusingly written there because it sure sounds like it's saying using useCallback but omitting a dependency is acceptable.
IMHO useCallback is still a good idea in those situations presuming you care about the potential needless re-renders (which for a lot of smaller apps probably aren't really a perf issue, so maybe you don't). If component A renders component B and does not memoize its own callback that it passes to B as a prop, that is A's problem, not B's. Memoization is easy to get wrong by forgetting to memoize one spot which cascades downwards like the screenshots example, but that doesn't mean memoization is never helpful or components shouldn't do their best to optimize for performance.
I'm not in frontend mainly, but this article, and another short I watched about how useEffect is almost always wrong makes me feel uneasy about React. It's insidiously complex.
A bunch of `useCallbacks` are rendered ineffective because the top of the chain wasn't properly computed. And the fix isn't `useCallback`, its to rearchitect the tree. This feels wrong - either the API, the framework, or the language isn't powerful enough to ensure that someone does the right and performant thing. It seems like the new React Compiler tries to fix this - but I'm surprised React Compiler is a thing that needs to be built.
React is the Git of UIs, the Vulkan, or whatever example you feel more apt for its ways of working, and changes across versions.
Unfortunely, it has won the mindshare game, in many SaaS products I tend to work on, they only provide React and by extension Next.js based SDKs, if I want to use vanila.js, Angular, Vue or whatever, it is on me to make it work, if it is possible at all, given the product expectations on its extension points.
Eventually the "everything must be React" wave is going to pass, hopefully.
Ahh, this is such a pet peeve of mine. I hate how it leaks. A thing I often also encounter in this vein is when you write a useEffect, but the linter forces you to declare everything in the dependency array. I only want the effect to trigger when x changes, and then call function y. But I have to declare y as well, but then if y isn't memoed it will trigger every render. But I don't want that, and I might not even have full control of function y, coming from somewhere else. I could usecallback around it myself first, but that just moves the problem, as I need to maintain the correct closure, or there will be subtle bugs.
A second but unrelated grief with hooks is when you get async updates happening in between renderers where you want to update some state and then do some logic. Let's say I'm waiting for 2 requests to finish. They both should update a value in my state. However, to not lose an update if both happens at the same time, I need to do the setState with a callback to see the latest value. But then, what if I want to trigger something when that new calculated state value is z? I can't do that imperatively inside the callback, and outside it I can't see the new value and don't know that I now should do something additional. I have to do it in a useEffect, but that decouples it too much, the logic belongs where I want to put it, in my state hook. Not in some useeffect wherever. The other option is to misuse refs to also maintain a current state there in addition to the useState. So always use the ref, but set the state in the end to trigger a re render.
Hooks are so unelegant for real problems. I hate how they force your hand to write spaghetti.
> The other option is to misuse refs to also maintain a current state there in addition to the useState. So always use the ref, but set the state in the end to trigger a re render.
Arugably this isn't a misuse, but just accepting that Reacts state management isn't powerful enough for what you need. Something akin to useRef + useSyncExternalStore is often what developers want.
For example, there's no guarantee that your useEffect will be invoked with the intermediate state you wanted to observe.
> I hope this example shows why I'm passionately against applying memoizations. In my experience, it breaks way too often. It's not worth it. And it adds so much overhead and complexity to all the code we have to read.
React's complexities are almost all self-inflicted footguns created as a byproduct of its design that points the reactive callback to the component function.
This design means that unlike vanilla JS, web components, Vue, Svelte, Solid, etc. where your state explicitly opts in to state changes, React requires component code to explicitly opt out of state changes (making the model much harder to wrap your head around and harder to do well).
This is the actual root cause of all of the underlying complexity of React. In signals-based frameworks/libraries and in vanilla JS with callbacks, there is rarely a need to explicitly memoize.
Most of the time it’s not with memoising anything because as it’s pointed out it’s easy to create a component that tries to be performant but its consumer breaks it because it passes a non memoized prop. And it is weird that a library that pushes for composability of UI components demands of you to think of such edge cases.
I have been writing react professionally in large codebases for 5 years now and I am quite disappointed by the course react followed. It became more complex but not more powerful, it introduced features that feel like bandaid fixes rather than new tools that open up new opportunities, it broke its own rules and patterns (the new “use”, the “useForm”) and it aggressively pushed for server side rendering, alienating hobbyists and small scale users who enjoyed SPAs.
Like another user mentioned, I am irritated by the linter rule for full exhaustive dependencies in effects, because it takes control away and also forces me to memoise everything. Or not, as the article points out. The library is not easy to build with anymore and it’s comfortably sitting at the point where it’s used everywhere and people are getting tired of its quirks. Which, if we are lucky, means it prepares the way for something better.
I still don't 100% understand why `React.memo` is not applied by default, wrapping every single component in a `memo` is crazy overhead and non-trivial to enforce codebase-wide.
Sure performance is a concern but is recursive re-rendering really cheaper than memoing all props?
The useRef pattern seem like a code smell, maybe because I had some troubles with refs over the years.
Things were simpler when we had lifecycle methods to manage some of this things. But I`m sure that the next version of react will change everything and make us come up with more patters to try to fix the same problem again...
I've been toying with an idea of a pattern. I'm curious as to if it has a name. I'll write a blog post once I have an app using it. In the meantime, it's (roughly):
- No Hooks.
- Don't try to sync any state. E.g. keep a dom element as the source of truth but provide access to it globally, let's call this external state.
- Keep all other state in one (root) immutable global object.
- Make a tree of derived state coming from the root state and other globally available state. (These are like selectors and those computations should memoized similar to re-select)
- Now imagine at any point in time you have another tree; the dom tree. If you try to make the state tree match to dom tree you get prop drilling.
- Instead, flip the dom tree upside down and the leaves get their data out of the memoized global state.
- Parent components never pass props to children, the rendered children are passed as props to the parent.
You end up with a diamond with all state on the top and the root of the dom tree on the bottom.
One note, is that the tree is lazy as well as memoized, there's potentially many computations that don't actually need to be computed at all at any given time.
You need something like Rx to make an observable of the root state, some other tools to know when the external state changes. Some memoization library, and the react is left with just the dom diffing, but at that point you should swap out to a simpler alternative.
you can do something like this with most global state libraries, Jotai to name one. But very soon you'll see that you need effects there, so you'll need that global state solution to be rock solid in that aspect
I would describe the reasons to useMemo or useCallback as follows, in order of decreasing importance: 1) referential equality of non-primitives (any object, function, etc.) passed downstream as props and 2) expensive computation. The first one is for me non-optional, and I’ll focus on that (as the second one is self-explanatory).
Once you know about the reactive render cycle, it becomes a second nature to never create any object other than via a memo function with declared dependencies. Anything else—any instance of something like <Component someProp={[1, 2, 3]} />—is, to me, just a bug on the caller side, not something to be accommodated.
Furthermore, ensuring referential equality has benefits beyond avoiding re-renders (which I would acknowledge should generally be harmless, although are in practice a source of hard to spot issues and loops on occasion).
— First, it serves as a clarity aid and documentation for yourself. It helps maintain a mental graph like “caller-provided props A and B are used to compute prop C for a downstream component”. Code organized this way tends to have self-evident refactor routes, and logic naturally localizes into cohesive sub-blocks with clearly defined inputs and outputs.
— Second, and it is obviously an ES runtime thing, but it is simply weird to have to reason about objects being essentially the same but different—and if memoization allows me to not have to do it and, with some discipline, make it safe to assume that (() => true) is the same as (() => true), why pass on that opportunity?
That said, now that I see an alternative take on this, I will make sure to document my approach in the development guidelines section of the README.
> it is obviously an ES runtime thing, but it is simply weird to have to reason about objects being essentially the same but different—and if memoization allows me to not have to do it and, with some discipline, make it safe to assume that (() => true) is the same as (() => true), why pass on that opportunity?
it sucks both ways - the default is that stuff is different if it looks the same so you can't not do anything, but the only solution is both a lot of work and really easy to mess up completely. The TC39 Composites proposal[1] would be a great step in the right direction, but it will take years in the best scenario, there's really no guarantee that React will take advantage of it, and it still leaves out a lot of cases (Dates, to name one)
I remember when React api was unique, then it comes up with some idea (hooks) and other front-end frameworks copied it, that's like confirmation that the idea is crap.
People say "don't just blindly use React.memo, fix slow renders first." But like... with referential identity being so cheap, blindly applying React.memo should be a huge win?
CostOfCache is pretty cheap compared to the CostPreCache IMO... CostWhenHittingCache is very low... and CacheMissPercentage is also probably pretty low in your typical React component that has more than 2 children node involved. Mathematically it feels like a no-brainer!
"Fix your slow renders first" just feels off. Yes you want to fix slow first renders! You also want to avoid wasting render cycles. These are distinct issues from my view. What am I missing? Why shouldn't React "just" do useMemo by default?
I don't have a problem with needing to memoize props passed to child components for their memoization to work.
If your parent component doesn't need the optimization, you don't use it. If it does need it, your intention for using useMemo and useCallback us obvious. It doesn't make your code more confusing inherently.
The article paints it as this odd way of optimizing the component tree that creates an invisible link between the parent and child - but it's the way to prevent unnecessary renders, and for that reason I think it's pretty self-documenting. If I'm using useMemo and useCallback, it's because I am optimizing renders.
At worst it's unnecessary - which is the point of the article - but I suppose I don't care as much about having unnecessary calls to useMemo and useCallback and that's the crux of it. Even if it's not impacting my renders now, it could in the future, and I don't think it comes at much cost.
I don't think it's an egregious level of indirection either. You're moving your callbacks to the top of the same function where all of your state and props are already.
A useless machine or useless box is a device whose only function is to turn itself off. The best-known useless machines are those inspired by Marvin Minsky's design, in which the device's sole function is to switch itself off by operating its own "off" switch. Such machines were popularized commercially in the 1960s, sold as an amusing engineering hack, or as a joke.
More elaborate devices and some novelty toys, which have an obvious entertainment function, have been based on these simple useless machines.
History
The Italian artist Bruno Munari began building "useless machines" (macchine inutili) in the 1930s. He was a "third generation" Futurist and did not share the first generation's boundless enthusiasm for technology but sought to counter the threats of a world under machine rule by building machines that were artistic and unproductive.
A wooden "useless box"
The version of the useless machine that became famous in information theory (basically a box with a simple switch which, when turned "on", causes a hand or lever to appear from inside the box that switches the machine "off" before disappearing inside the box again) appears to have been invented by MIT professor and artificial intelligence pioneer Marvin Minsky, while he was a graduate student at Bell Labs in 1952. Minsky dubbed his invention the "ultimate machine", but this nomenclature did not catch on. The device has also been called the "Leave Me Alone Box".
Minsky's mentor at Bell Labs, information theory pioneer Claude Shannon (who later became an MIT professor himself), made his own versions of the machine. He kept one on his desk, where science fiction author Arthur C. Clarke saw it. Clarke later wrote, "There is something unspeakably sinister about a machine that does nothing—absolutely nothing—except switch itself off", and he was fascinated by the concept.
Minsky also invented a "gravity machine" that would ring a bell if the gravitational constant were to change, a theoretical possibility that is not expected to occur in the foreseeable future.
This reminds me of a coworker that was writing a program for some late 80's based DSP accelerator. He tried compiling the thing and nothing happened for a few minutes and then it printed out 'too many errors'.
When he went to edit his program it had been deleted. So he rewrote it and tried compiling it again and found the compiler had deleted itself too.
I like the idea of a program that takes increasingly dire steps to prevent you from compiling it each time you try.
I know that the conventional wisdom (e.g. https://kentcdodds.com/blog/usememo-and-usecallback) is not to do that, but it's worth noting that Kent doesn't actually bring receipts for his claims, and also the discussion is typically limited to toy examples, not huge applications that have the potential for huge cascading renders when anything changes. In my own profiling of real world applications I have found that memoizing everything is not actually a performance regression. And then also consider the React Compiler, which of course does the same, at an even more fine-grained level than any human could be bothered to consistently do by hand.
Whenever I see articles like that, I can't help but feel the migration from Class components to functional components with hooks was a big complexity jump for little gain.
I think folks were too quick to throw out the good things about redux when hooks came out. Namely “dumb” presentational components that had no logic and abstractions for wiring up data and actions (mapStateToProps and mapDispatchToProps).
Sure reducers could be a bear (and asynchronous actions were hard), but the easily testable and portable logic-less views were really nice to work with.
Having worked in a number of codebases from React’s earliest days until now, I see echos of the bad old mixin days in the usage of hooks.
28 comments
[ 3.7 ms ] story [ 35.8 ms ] threadI think this is wrong? If you memoize a callback with useCallback and that callback uses something from props without putting it in the dependency array, and then the props change & the callback runs, the callback will use the original/stale value from the props and that's almost certainly a bug.
They might be trying to say you just shouldn't use useCallback in that situation, but at best it's very confusingly written there because it sure sounds like it's saying using useCallback but omitting a dependency is acceptable.
IMHO useCallback is still a good idea in those situations presuming you care about the potential needless re-renders (which for a lot of smaller apps probably aren't really a perf issue, so maybe you don't). If component A renders component B and does not memoize its own callback that it passes to B as a prop, that is A's problem, not B's. Memoization is easy to get wrong by forgetting to memoize one spot which cascades downwards like the screenshots example, but that doesn't mean memoization is never helpful or components shouldn't do their best to optimize for performance.
A bunch of `useCallbacks` are rendered ineffective because the top of the chain wasn't properly computed. And the fix isn't `useCallback`, its to rearchitect the tree. This feels wrong - either the API, the framework, or the language isn't powerful enough to ensure that someone does the right and performant thing. It seems like the new React Compiler tries to fix this - but I'm surprised React Compiler is a thing that needs to be built.
Unfortunely, it has won the mindshare game, in many SaaS products I tend to work on, they only provide React and by extension Next.js based SDKs, if I want to use vanila.js, Angular, Vue or whatever, it is on me to make it work, if it is possible at all, given the product expectations on its extension points.
Eventually the "everything must be React" wave is going to pass, hopefully.
A second but unrelated grief with hooks is when you get async updates happening in between renderers where you want to update some state and then do some logic. Let's say I'm waiting for 2 requests to finish. They both should update a value in my state. However, to not lose an update if both happens at the same time, I need to do the setState with a callback to see the latest value. But then, what if I want to trigger something when that new calculated state value is z? I can't do that imperatively inside the callback, and outside it I can't see the new value and don't know that I now should do something additional. I have to do it in a useEffect, but that decouples it too much, the logic belongs where I want to put it, in my state hook. Not in some useeffect wherever. The other option is to misuse refs to also maintain a current state there in addition to the useState. So always use the ref, but set the state in the end to trigger a re render.
Hooks are so unelegant for real problems. I hate how they force your hand to write spaghetti.
Arugably this isn't a misuse, but just accepting that Reacts state management isn't powerful enough for what you need. Something akin to useRef + useSyncExternalStore is often what developers want.
For example, there's no guarantee that your useEffect will be invoked with the intermediate state you wanted to observe.
This design means that unlike vanilla JS, web components, Vue, Svelte, Solid, etc. where your state explicitly opts in to state changes, React requires component code to explicitly opt out of state changes (making the model much harder to wrap your head around and harder to do well).
This is the actual root cause of all of the underlying complexity of React. In signals-based frameworks/libraries and in vanilla JS with callbacks, there is rarely a need to explicitly memoize.
For the curious, I have some code samples here that make it more obvious: https://chrlschn.dev/blog/2025/01/the-inverted-reactivity-mo...
I have been writing react professionally in large codebases for 5 years now and I am quite disappointed by the course react followed. It became more complex but not more powerful, it introduced features that feel like bandaid fixes rather than new tools that open up new opportunities, it broke its own rules and patterns (the new “use”, the “useForm”) and it aggressively pushed for server side rendering, alienating hobbyists and small scale users who enjoyed SPAs.
Like another user mentioned, I am irritated by the linter rule for full exhaustive dependencies in effects, because it takes control away and also forces me to memoise everything. Or not, as the article points out. The library is not easy to build with anymore and it’s comfortably sitting at the point where it’s used everywhere and people are getting tired of its quirks. Which, if we are lucky, means it prepares the way for something better.
Sure performance is a concern but is recursive re-rendering really cheaper than memoing all props?
Things were simpler when we had lifecycle methods to manage some of this things. But I`m sure that the next version of react will change everything and make us come up with more patters to try to fix the same problem again...
One note, is that the tree is lazy as well as memoized, there's potentially many computations that don't actually need to be computed at all at any given time.
You need something like Rx to make an observable of the root state, some other tools to know when the external state changes. Some memoization library, and the react is left with just the dom diffing, but at that point you should swap out to a simpler alternative.
Once you know about the reactive render cycle, it becomes a second nature to never create any object other than via a memo function with declared dependencies. Anything else—any instance of something like <Component someProp={[1, 2, 3]} />—is, to me, just a bug on the caller side, not something to be accommodated.
Furthermore, ensuring referential equality has benefits beyond avoiding re-renders (which I would acknowledge should generally be harmless, although are in practice a source of hard to spot issues and loops on occasion).
— First, it serves as a clarity aid and documentation for yourself. It helps maintain a mental graph like “caller-provided props A and B are used to compute prop C for a downstream component”. Code organized this way tends to have self-evident refactor routes, and logic naturally localizes into cohesive sub-blocks with clearly defined inputs and outputs.
— Second, and it is obviously an ES runtime thing, but it is simply weird to have to reason about objects being essentially the same but different—and if memoization allows me to not have to do it and, with some discipline, make it safe to assume that (() => true) is the same as (() => true), why pass on that opportunity?
That said, now that I see an alternative take on this, I will make sure to document my approach in the development guidelines section of the README.
it sucks both ways - the default is that stuff is different if it looks the same so you can't not do anything, but the only solution is both a lot of work and really easy to mess up completely. The TC39 Composites proposal[1] would be a great step in the right direction, but it will take years in the best scenario, there's really no guarantee that React will take advantage of it, and it still leaves out a lot of cases (Dates, to name one)
[1]: https://github.com/tc39/proposal-composites
CostPostCache = CostOfCache + CacheHitPercentage * CostWhenHittingCache + CacheMissPercentage * CostPreCache
CostOfCache is pretty cheap compared to the CostPreCache IMO... CostWhenHittingCache is very low... and CacheMissPercentage is also probably pretty low in your typical React component that has more than 2 children node involved. Mathematically it feels like a no-brainer!
"Fix your slow renders first" just feels off. Yes you want to fix slow first renders! You also want to avoid wasting render cycles. These are distinct issues from my view. What am I missing? Why shouldn't React "just" do useMemo by default?
If your parent component doesn't need the optimization, you don't use it. If it does need it, your intention for using useMemo and useCallback us obvious. It doesn't make your code more confusing inherently.
The article paints it as this odd way of optimizing the component tree that creates an invisible link between the parent and child - but it's the way to prevent unnecessary renders, and for that reason I think it's pretty self-documenting. If I'm using useMemo and useCallback, it's because I am optimizing renders.
At worst it's unnecessary - which is the point of the article - but I suppose I don't care as much about having unnecessary calls to useMemo and useCallback and that's the crux of it. Even if it's not impacting my renders now, it could in the future, and I don't think it comes at much cost.
I don't think it's an egregious level of indirection either. You're moving your callbacks to the top of the same function where all of your state and props are already.
Useless Machine:
https://en.wikipedia.org/wiki/Useless_machine
A useless machine or useless box is a device whose only function is to turn itself off. The best-known useless machines are those inspired by Marvin Minsky's design, in which the device's sole function is to switch itself off by operating its own "off" switch. Such machines were popularized commercially in the 1960s, sold as an amusing engineering hack, or as a joke.
More elaborate devices and some novelty toys, which have an obvious entertainment function, have been based on these simple useless machines.
History
The Italian artist Bruno Munari began building "useless machines" (macchine inutili) in the 1930s. He was a "third generation" Futurist and did not share the first generation's boundless enthusiasm for technology but sought to counter the threats of a world under machine rule by building machines that were artistic and unproductive.
A wooden "useless box"
The version of the useless machine that became famous in information theory (basically a box with a simple switch which, when turned "on", causes a hand or lever to appear from inside the box that switches the machine "off" before disappearing inside the box again) appears to have been invented by MIT professor and artificial intelligence pioneer Marvin Minsky, while he was a graduate student at Bell Labs in 1952. Minsky dubbed his invention the "ultimate machine", but this nomenclature did not catch on. The device has also been called the "Leave Me Alone Box".
Minsky's mentor at Bell Labs, information theory pioneer Claude Shannon (who later became an MIT professor himself), made his own versions of the machine. He kept one on his desk, where science fiction author Arthur C. Clarke saw it. Clarke later wrote, "There is something unspeakably sinister about a machine that does nothing—absolutely nothing—except switch itself off", and he was fascinated by the concept.
Minsky also invented a "gravity machine" that would ring a bell if the gravitational constant were to change, a theoretical possibility that is not expected to occur in the foreseeable future.
When he went to edit his program it had been deleted. So he rewrote it and tried compiling it again and found the compiler had deleted itself too.
I like the idea of a program that takes increasingly dire steps to prevent you from compiling it each time you try.
{} !== {}
and that it uses reference equality as a proxy for value equality.
This is why the absolute simplest thing to do is to just memoize all the things (see https://attardi.org/why-we-memo-all-the-things/ for a deeper dive into this approach).
I know that the conventional wisdom (e.g. https://kentcdodds.com/blog/usememo-and-usecallback) is not to do that, but it's worth noting that Kent doesn't actually bring receipts for his claims, and also the discussion is typically limited to toy examples, not huge applications that have the potential for huge cascading renders when anything changes. In my own profiling of real world applications I have found that memoizing everything is not actually a performance regression. And then also consider the React Compiler, which of course does the same, at an even more fine-grained level than any human could be bothered to consistently do by hand.
Sure reducers could be a bear (and asynchronous actions were hard), but the easily testable and portable logic-less views were really nice to work with.
Having worked in a number of codebases from React’s earliest days until now, I see echos of the bad old mixin days in the usage of hooks.