Understanding UseMemo and UseCallback
joshwcomeau.com
joshwcomeau.com
I only rant about this here because I think useCallback is pretty awful design - separate functions that only exist as syntactic sugar are terrible for user readability. A lot of people probably know useMemo as it's quite easy to understand and used somewhat frequently, but if I were to use useCallback in my code it would probably cause a lot of others at my work to then have to go scouring the docs to figure what the heck this hook exactly does. On the other hand, just forcing users call useMemo and return a function means that everyone who knows useMemo knows what's happening.
Unless I'm completely misunderstanding how useCallback works, perhaps React actually does some extra optimization under the hood or for this or something.
From what I'm aware, it's as you said. It's simply a syntactic sugar for useMemo if you want to memoize functions.
Now if useCallback doesn't exist, you have to use useMemo and pass in a function to the function. Something like `useMemo(() => () => ...)`. Sure if you know what useMemo does then you'll know what it does. But I have a hard time believing that juniors would find that easier to read than useCallback(() => ...).
All I need to say to them is: "Hey, if you want to memoize a function, use useCallback instead". And if I see useCallback used anywhere, I know for sure that it's memoizing a function. Rather than saying: "So you see that there's an extra bracket there? That means that you're returning a function. Yada yada"
There's a grand total of 15 hooks[1], most of which you probably never ever have to use. I would expect all React developer to go through them all to see what they're about.
I would also expect them to know & remember most of the basic ones: useState, useEffect, useContext, useReducer, useCallback, useMemo and useRef. That's a total of 7 functions. If they can't do that then I question the quality of today's developers.
Exactly. I’d rather write the code myself than have to review the code of someone who can’t understand such a basic set of concepts.
In JavaScript it's document.getElementById and set attribute. In react it's easy to cause part or the entire list to recalculate the virtual dom.
I couldn't think of a good solution. Ideally I'd just call set state on the child element that needs updating but that would require registering all the relevant functions with the parent. Super ugly.
If so... can the entire list be stored in state (and updated as necessary), but only the ones marked "is visible" actually get rendered?
Maybe I'm misunderstanding completely...
In react because the state flowed from top to bottom I have to be very careful about what objects I update. Just passing down a single reveal could force the virtual dom to update for (num children per node)*depth. Even this required care on what props got updated.
The solutions seemed to be: 1. lots of useMemo so only a minimal amount of a node got recalculated; 2. Directly call a set state function of another child.
Both are quite ugly :( but the second seems nicer as it never could force a full update. However, it seemed to require a registry of set state functions {id: setState}.
I'd like to have some citation here. Most of the product startups use React for their frontend, and they seem to make it work really well. Never saw any issues.
I work as a freelance FE dev for 8 years and been doing webdev since the late 90s. I must say that for SPAs, React (with hooks) does the best job at scaling up (if you pay attention to your architecture) and I have seen and used frameworks/libs come-and-go such as knockoutJS, jquery, angular1, angular2+, (p)react etc.
Absolutely agree. But this wasn't always the case. The original class components were much easier to understand. A backend developer could open a .tsx file and understand what is going on, and make simple fixes. This was one of the strengths of React - it was so clean, compared to Angular and other frameworks.
But then they introduced hooks and messed it up. Now it is not intuitive in the least. Hooks makes some things easier but the cost in terms of loss-of-intuitiveness is huge. The other mistake Facebook is making is assuming that performance is the most important attribute of a UI framework. It is not. For 99.9% of developers the original React was already fast enough. They should have focused on making the mental model cleaner, and smoothing some of the roughness of the lib such as updating the props of a stateful component, handling deeply nested objects in the state without making entire copies of the data and so on.
Hooks compose, whereas side effects and memoized values sprinkled through component constructors and lifecycle methods do not.
For example, the equivalent of useEffect required calls inside of componentWillMount, componentDidUpdate, and componentWillUnmount. You try and make something like this re-usable and you’ll be leaking details of your implementation across the whole component via inclusion in these lifecycle methods, not to mention any data you’re shoving onto the component instance. But it’s still doable.
Now, what if you wanted to use this re-usable behavior inside of another re-usable behavior? It gets complicated fast! Now your library needs to expose the lifecycle-updating methods of the underlying library, leaking details all the way down. Hooks are opaque from the perspective of lifecycle, while still having access to all the same… hooks.
> For example, the equivalent of useEffect required calls inside of componentWillMount, componentDidUpdate, and componentWillUnmount.
Yeah from what I recall this was the prime motivational example given in the React docs for introducing hooks. It's a legit problem. But hooks and getting rid of class based components is not the only solution. In classical OO are also well worn solutions along the lines of hooks, they just work with classes / objects with interfaces still. It's somewhat frustrating that this is never even considered.
Ironically they could have prevented this by making them more functional. Instead of some magic hidden state, you should have to always pass a context to them, and you should have to give every hook a unique name or a Symbol().
Class components only made you feel like they were easier to understand because the method names indicated an intuitive lifecycle. But the reality under the hood was almost certainly different than your mental model because the method/event handler names were actually deceptive. Hooks are much closer to the metal, so to speak.
> A backend developer could open a .tsx file and understand what is going on, and make simple fixes.
They might assume they knew what was going on and how their "simple fixes" would affect execution, but that would probably be naïve. But that goes for anyone jumping into any codebase for a platform/library that they're not familiar with.
> Hooks makes some things easier but the cost in terms of loss-of-intuitiveness is huge.
This is fair, although I feel like the learning curve for React is only steep at first, then plateaus for the vast majority of application developers. It only gets really complicated (as anything does) when you start to get into advanced performance tuning or you're writing a library with very specific requirements.
> The other mistake Facebook is making is assuming that performance is the most important attribute of a UI framework.
This is simply not true. The React team has indicated many times over the years that basic, hello-world level, idiomatic React code is fast enough for the vast majority of applications ("don't worry too much about the number of re-renders," etc.). They're very anti-premature optimization as a group.
Also, Hooks weren't introduced to improve performance anyway (at least that wasn't the primary motivation).
> handling deeply nested objects in the state without making entire copies of the data
It's not from the React team, nor is it specific to React, but have you tried immer? https://immerjs.github.io/immer/
I've never worked on a codebase where this isn't true regardless of what frameworks or libraries are used - that's kinda part of the gig.
But with React, every dev ends up doing the same thing their own way, with subtle differences and subtle bugs, and then as turnover happens and frameworks become less fashionable, codebases get polluted by a mix of different dev and tool generations and it gets even harder to ensure correctness.
Using a more opinionated framework built on top of React, like Next.js, can solve a lot of those issues... until Next itself becomes unfashionable and the cycle starts again :(
> It's a lot harder to maintain over time than a more opinionated framework.
That's not true at all, that depends on the quality of the codebase the exact same as if it were built on an opinionated framework. I've seen plenty of bloated Rails codebases that were miserable to maintain.
<button onClick={() => setText("clicked")}>click</button>
Without useCallback this would update the dom on every render.
My best advice is (also apart from React): Do not spend time on imagined/anticipated performance bottlenecks; only ever spend time on performance tuning if there are real (perceived, not measurable!) performance issues (there has to be a profiling report in your hand).
I believe this causes a real Dom update because it can't know the onclick function hasn't materially changed. It's just diffing function references.
setText doesn't change because it's from useState. The only reason to update the Dom is because the onclick function isn't memoized.
Updating the Dom every render isn't terrible but it isn't good.
https://www.joshwcomeau.com/react/why-react-re-renders/
FB should hire him to rewrite their docs.
One thing I’ve started doing everywhere is putting a logger.debug(“rendering WidgetX”) at the top of every component to catch anything weird as I go along, it’s been pretty helpful.
Maybe I can separate the hooks into sets the data and reads the data somehow…
Might just return to redux to be honest for this bit at least…
A stateless programming model in a domain where state is so fiercely coupled to program utility is inevitably going to spawn this garbage.
Class based components with MobX managing state was, and still is, a dream to write __and__ read.
Literally, calling ‘useMemo’ twice returns two different objects, because the function keeps track in internal state of how many times it has been called.
Hooks are very deeply imperative.
They are a way of obtaining some benefits that are easily obtained in functional programming via higher-order functions, but expressed in a way that makes them easily consumed in an imperative code body.
React has always been an exercise in smoke and mirrors.
The solution for more complex useMemo and useCallback usage is to create your own hooks.
You can compose all the hooks into a single hook that your component uses and return from that hook whatever your component needs. This is common with components that have a lot of event listeners that can trigger local state and/or redux state changes.
We have a spreadsheet component and the individual cells had some pretty gnarly hooks and after pulling them into their own custom hook we've been able to maintain them nicely.
If you don't wrap the child component with "React.memo", every time the parent renders the child will render regardless of prop equality, even with memoized props/callbacks.
Adding useCallback for functions only does something useful if you also React.Memo those children. So while this is a very important optimization, it's not one you need to apply to everything.
It only exists/is done to give you a stable reference so you can avoid triggering downstream dependencies (hook arrays and React.memo'd components)
One reason useCallback exists is because useMemo, too, allocates a function on every render- even if it doesn't recompute the underlying value by actually calling the function. So if you're going to be allocating a function anyway, there's no point (performance-wise) to allocating a function that creates a function. You can just cut out the middle-man
The virtual DOM is a tree, and if roughly balanced, the path that needs to be updated is of size log(N) compared to the full tree of size N that will be updated by default.
In practice, it's the difference between a stuttering app that lags at every keypress in an input field, and something usable.