Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Why React Re-Renders (joshwcomeau.com)
319 points by clessg on Aug 16, 2022 | hide | past | favorite | 162 comments


Josh's content is always very very high quality. I look forward to him releasing his online React course [0] so that I can recommend it to others who are starting out.

My personal biggest not-total-comprehension is around Hooks / effects. I've followed tutorials, used them in production, etc. I'm comfortable using them but I also consider them a bit of a black box, which I don't like (e.g. I'm not sure how they're implemented). The other (even bigger) question for me is "why" -- what prompted the React team to change everything over to "effects". Anyone?

[0] https://www.joyofreact.com/


> e.g. I'm not sure how they're implemented

It might help to have a simple mental model for them?

Picture there is a global variable called _currentComponent:

  _currentComponent: {
    previousValue: React.Element
    hooks: HookValue[]
    currentHook: number
    firstRender: boolean
  }
Each HookValue is whatever that hook wants. For useState something like:

  HookValue = {
    hookName: 'useState',
    value: [state, setState],
  }
Before your component is run, the renderer sets `_currentComponent` to your component with its hooks set to their current values.

If you run `useState(initial)` it looks like this:

  function useState(initialValue) {
    const component = _currentComponent
    let hookValue;
    if (firstRender) {
      hookValue = {
        hookName: 'useState',
        value: [initialValue, (newValue) => {
          hookValue[0] = newValue
          markForUpdate(component); // some React-provided function to mark this as needing an update
        }
      }
      component.hooks[component.currentHook++] = hookValue;
      return hookValue.value
    } else {
      hookValue = component.hooks[component.currentHook++]
      assert(hookValue.hookName == 'useState')
      return hookValue.value
  }
Surely not perfect, but totally useable as a model of what's going on. To be honest I wish React showed pseudocode like this in the tutorials, it makes it a lot easier for me to understand.


Yep! Also see Shawn Swyx Wang's talk "Getting Closure on React Hooks", where he goes through an equivalent example in more detail:

https://www.swyx.io/hooks/


Thank you for posting this.


Hooks feel like a good system that stopped right before it became pretty. Like, an componentDidMount event is a now useEffect with no second argument? But a componentWillUnmount event is now a useEffect, with no second argument, that returns a callback?

It's powerful, and it's not that hard to use, but it's cryptic and random.

Why call it useEffect instead of some other more meaningful phrase? I mean, "componentDidMount" tells you exactly what it is. Does "useEffect"?

Why should onload things be a function, but onunload should be a function returned by a function?

Why does useRef give you a thing used for attaching handles to DOM elements so you can refer to them elsewhere, AND it gives you an object whose .current property can be used as a variable that persists without causing a render?

It's like someone complained that there were two many functions in the API, and their names were too long, so they overcorrected in the opposite direction.


You're translating the class-based way of working in React against hooks, without stopping to consider understanding hooks as a first-class principle instead of a translation.

It's called useEffect because it runs on the (side)effects observed by the dependency array. An empty array happens to happen on mount. useEffect returns a teardown function which happens to correlate with unmount when an empty array is passed.

It's called useRef because it returns a (mutable) reference that's detached from the reactive layer. The DOM element connection is just sugar.

I agree that useEffect has a lot of footguns, but this position seems very shallow. The whole pattern changed, it doesn't make a lot of sense to continue comparing the two


> useEffect returns a teardown function which happens to correlate with unmount when an empty array is passed.

I believe this teardown function runs on unmount whether or not the dependency array is empty.


Yes, you're correct, and he got it wrong when talking about it.

This kind of goes towards my point. useEffect works, and it works great. It's flexible and powerful - but it's cryptic as hell, and the interface is just "memorize it because you have to", not something more intuitive.

What if useEffect required a 2nd argument, but you had the option to pass in self, which would still offer the onMount behavior, by mirroring the way the other form of it worked? Bada-bing, now there's no exception to the rule. What if the 2nd argument was a named argument, like "trigger"? Now it's self-documenting. What if the teardown function as an optional 3rd argument with a name, too? Now it's easy to glance at a useEffect and tell whether it's declaring a teardown without carefully reading the body. etc etc.

He says "useRef offering a connection to DOM elements is just sugar", but that's not sweet, to me. There's no reason one thing should do two totally different things. That's exactly what I'm complaining about.


In the beginning React was a great example of high payoff of just a little bit of "memorize it because you have to."

For many projects it felt like it gave you super powers, for some it was just OK, and a small percentage of interfaces just didn't fit the React model and were better some other way.

It appears to me that since then that small percentage has gotten the development attention (understandably), creating a React which is more well rounded and broadly useful but there is more to learn and the super power feeling has been dulled a bit.

Overall it has improved but I still wonder what a React that was more specialized for those interfaces where it really works great would be like.


Oh hey, that's me! :wave:

I definitely agree useEffect is awfully cryptic and easy to get wrong, and I'm happy to be corrected... but I'm not sure how I'm wrong about it running on every render when there's no 2nd argument?

I kinda wish they'd made it easier to do the common operations like onMount and onUnmount by providing some simplified wrappers around useEffect (with less power comes less responsibility... or something). Of course we can make custom hooks for those, but having the well-trodden paths be paved is always nice.


I misunderstood what you were saying about it running on unmount when the dependency array is empty.

Of course, it does run on unmount when the dependency array is empty, and it runs on unmount when there are dependencies in the dependency array.

But upon re-reading what you said, I think your intent was that it's comparable to componentWillUnmount() in class-based React only when the dependency array is empty (because otherwise the cleanup function also gets called when dependencies change).

My apologies, as I never really used class-based React, so the distinction was lost on me (and also forgot about the cleanup function being called between useEffect callback calls)


Oh gotcha, no worries. I'm not sure if you meant to reply to me anyway, my comment was in regards to knodi123's original comment that linked to an article of mine and said it was wrong about when useEffect re-renders – but the comment was edited and that part is gone, so now mine looks entirely crazy haha.


I edited it because I realized I was wrong - I was confused by the difference between no 2nd argument, and [] for the 2nd argument. But again - as a full time professional web developer, who has being using React for a few years, I think that also goes towards my point. ;-)


Hahah, yeah, this whole set of threads just hammers home your point, really :D


useRef does only one thing. It returns an object where the latest value is current. That’s all. Element components will use a ref to put a reference to themselves if you pass one via the ref prop. They can be used to store anything else too. Nothing magic or confusing.

If anything it’d be weirder to make this two separate concepts. What would they even be? What does a ref not already do, as a box that contains an arbitrary value, that we’d want from a box with an element in it?


This is correct, but I think the point nickdandakis was trying to make is that useEffect does not directly correlate with componentDidUnmount because useEffect's returned callback could be called in the middle of the component's lifecycle, not only when the component unmounts.


Err.. no, you pass a callback (let's call it the effect callback) that returns a callback (the cleanup callback) to useEffect. The effect callback gets called when the component mounts, and when dependencies (if any) change. The cleanup callback gets called when the dependency unmounts (and will not be called at other times in the lifecycle)

edit: I'm wrong, the cleanup is also called when dependencies change, just not on mount.. eg. it gets called before the effect callback being called again.

In other words, the cleanup function is called on unmount when useEffect is called with an empty array, and if useEffect has dependencies, it will also be called when those dependencies change


It does, but it runs on more than just unmount when the dependency array is not empty. It runs anytime any dependency in the array changes. This is an important distinction. For example, event listeners added in useEffect will be removed anytime a dependency changes. Then a re-render will occur and they will be added back in the next useEffect body execution. This differs from componentDidUnmount which obviously only runs on unmount and never in the render cycle


I love hooks, but I think they made a few mistakes. The weirdness around useEffect having different behavior with no second argument vs [] is one of them.

And they should have included a useUnloadEffect() by default, even though it's trivial to write, just for clarity. It's way too easy to miscount the number of ()s in useEffect(() => () => {}, []);

They also should have included a few other basic hooks, like useStableValue() for just computing a constant once on first render.

I hate the name `componentDidMount` though. useEffect seems much better to me.


It seems like almost always if you want to do anything during unmount, it's cleaning up something you set up in a useEffect call, which you also want to do any time the useEffect call's dependency list changes, so it being part of the useEffect hook helps programmers fall into the pit of success. React's older class-based components had the unmount handling separate (componentWillUnmount), and in my experience on a large React codebase, many if not most components using componentWillUnmount were subtly flawed and failed to handle certain cases where props or state updated in a way that should have caused effects to be cleaned up or adjusted. React's useEffect hook way of grouping up effects with their own cleanup code helps people handle these cases correctly even without realizing it sometimes.


useRef is useStableValue


Not quite, having to use for current everywhere is just annoying, and it doesn't allow for a one-time constructor function.


Why do you have to use current everywhere? You can do const { current: myStableValue } = useRef(computation);

Not sure what you mean by one time constructor function, but you can pass the result of a function to initial value.

const { current: myStableValue } = useRef((() => { //called once })());


This is incorrect. useRef takes an initialValue argument, but it does not treat functions as lazy initializers like useState does. If you pass a function to useRef, you’re just going to get that function as the initial value.

If you want to lazily initialize a ref, you need to manually check if it has been initialized and run your expensive code if it hasn’t. Dan Abramov provides what appears to be a pattern officially recommended by the React team: https://github.com/facebook/react/issues/14490#issuecomment-...


I always do `const [stableValue] = useState(() => ...)`. It’s clear, simple, and easy to promote to an interactive value.


It's not a function, it's an immediately-invoked function expression.

Edit: I guess it would be a bit inefficient to invoke the IIFE every render just to initialize the value once. Probably better to use useMemo with [] as the dependency list then, either one achieves the same result.


I missed that you were immediately invoking the function expression. But yes, that doesn't save you from running the expensive calculation every subsequent render.


> Like, an componentDidMount event is a now useEffect with no second argument?

Doesn't useEffect with no second argument run after every render? componentDidMount's equivalent is for an empty dependency array in the second argument?


Bingo. And this is the situation we are in. Even knowing basically what's going on and still getting it wrong with just the bare minimum choices of ,_ or ,[].

Mix in actual deps coming from other hooks, changing as the components re-render, and multiple layers of "custom hooks" with more of the same and it's like trying to hold back a tsunami.


They could've easily added useOnMount, useOnRender, etc

It doesn't matter that they are sugar. Just like useState is built on useReducer, it would be massively helpful to simplify and clarify what's going on.


They very purposefully didn't add useOnMount because of the subtle bugs developers introduce when they do things only on mount. People would react to the initial props in componentDidMount, but never respond to their change.

Hooks, and useEffect, was supposed to be designed in a way to eliminate that class of bugs.


Sure but these functions would be trivial to define in one's own code base if they so desired.


I totally agree about the cryptic-ness when it comes to useEffect and its usage, but the neat thing is that you can trivially write your own thin wrappers with better names! I think `useEffect` is the best name possible given how broad its use cases are, because it's literally for any function side effect.


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.


I wish the current detractors would look back at this.

The options with life cycles are either huge life cycles with interspersed features OR super painful composition. I've been in big codebases with both and it was absolute hell.

Hooks aren't perfect or pretty but the fact that they can encapsulate AND compose makes them a million times better.

Those codebases are much easier to deal with now. Again, not perfect, but much better.


hooks were created to replace the functionality of mixins, not necessarily to replace lifecycles The react team could have easily added a useDidUpdate() and useWillMount(), but they didn't because the new abstractions cover the mixins use-cases also work better than lifecycles as well (but somewhat less intuitive at first glance)


You can reuse hooks between multiple components. You can't really do that with the lifecycle methods of class-based components.


you can with mixins

doesn't mean you should...


They are yet more evidence that any human engineered system will inevitably 'diverge' to become the most complicated thing that just about works - what we might call the complexity horizon.

Driven by the power of our 'ingenuity', even the most elegant constructions will find themselves drifting towards this horizon. It takes super human effort to resist the effect, and super humans to keep systems from escaping us.


Really? Hooks seem quite simple to me conceptually - certainly significantly more simple than class components were. If anything, they seem to me to be a clear disproof of your hypothesis.


Human-built systems’ complexity evolves until the limit of our IQ. Hire 140-IQ people, the systems will just evolve 20% further.


why? i personally like the "tear down" aspect effects. often times you have to clean up/unregister things when props change or component is unmounted. this leads to 5 random initializers in component class ctor and 5 random cleanups in onUnmount or whatever it was called. they're separated so its hard to correlate them. but even worse is that you cant encapsulate that by creating your own useX function. you could create HOCs but those were kind of clunky


> I look forward to him releasing his online React course

Any suggestions for a "second best" course that is available now?


Scrimba is pretty great in my opinion -- https://scrimba.com/allcourses?topic=react (not affiliated, I just think their online learning environment is neat)


Dan had a good summary of why hooks happened which I can't find now, the tldr was that there was a bunch of bugs introduced around referenced props/state being stale and unpredictable in components. Hooks was the natural evolution that removed all possibility of accidentally referencing stale data.


> Hooks was the natural evolution that removed all possibility of accidentally referencing stale data.

It unfortunately does not, you can hve various foot guns with refs or omitting dependencies from the array argument; but to your point it makes it a lot easier to ensure you do it correctly by collocating logic related to each cross cutting concern instead of entangling the code in the lifecycle methods.

More precisely it makes it easier to follow correct patterns, but as always that is subjective and hooks can be contentious :)


React would have been great in a functional language with immediate data structures.

Instead it’s used with JavaScript, where you have to constantly force yourself to create your prop objects in a very specific way to avoid or force (re)rendering.

It’s a constant struggle to control re-rendering. I still can’t believe React became as big as it is.

Vue and Angular just fit JavaScript better.


> immediate data structures

Immutable?

But React in JS is a blessing – it brought good thinking to messy world. And as a direct influence, JS will adopt immutable data structures – tuples and records.

https://github.com/tc39/proposal-record-tuple


I just cannot wait for this to come to the browsers. This will massively (and positively) impact how I can write JS.


I keep raving about it to people, no one cares or understands why this is MASSIVE for react development...


It's very widely used in the ClojureScript world (via Reagent) where it indeed has a better impedance match.



And sort of in Clojure with cljfx (React-like on JavaFX)

https://github.com/cljfx/cljfx


MobX is exactly what you're looking for: https://github.com/mobxjs/mobx

Vue works similar to React + MobX, but with weird design decisions like implicit reactivity, and a stale ecosystem thanks to the Vue2 vs Vue3 debacle.


The big part that seems to thoroughly confuses developers new to React is the difference between rendering and reconciliation. This is not particularly difficult to understand, but the original emphasis on the virtual DOM seems to lead to a misunderstanding on how React works. The vDOM plays a role only after rendering happened, so all that diffing stuff doesn't have anything to do with rendering. To me that seems like one of the primary causes of developers being surprised that React rerenders more than they expect.

I like the explanation on what triggers rendering in this post. Props and state are usually used in the explanation for this part, but props actually only matter if you want to use React.Memo. And the one part every React developer should know is that in the absence of React.Memo all children will rerender if any state changes.


yeah, this is key. What you should be REALLY avoiding is unnecessary DOM updates, rerenders have a cost too, but marginal compared to DOM updates

and even then you mostly don't need to care either except in extreme situations (like useCallback is not really necessary most of the time0


A pain point with React is large data structures. To re-render (assuming class components), you can setState with the changed property. For example if your state has two properties named foo and bar, and bar has changed then you call setState({ bar: newValue }). This works if you have simple properties. What if you have large complex data structures, and you need to modify a property deep down inside? Then you can make a copy of the data structure, then modify the field in the copy, then call setState({ bar: copyOfLargeObject }). But it is tremendously wasteful to make a complete copy of a large data structure!

A workaround is to not make copies of large data structures. Just modify the large object directly. Then just call setState({}); That's right... setState() with an empty object triggers a re-render. Now you don't even have to store the object being modified in state. You can hold the large object in a member field of the class. So even though your component is stateful, you are not telling React what your state fields are - you are managing it yourself. At this point, React's programming model has broken down.


FWIW, React has always been designed around some Functional Programming type principles, such as immutable updates.

Immutable updates do in fact require that if you want to update `state.some.nested.field`, you have to make copies of _all_ objects in that path: `nested`, `some`, and `state`. This isn't unique to React.

Yes, class component `this.setState()` lets you get away with mutations. That's not really a _good_ thing. If anything, it's a holdover from an earlier era of JS, where it was much more common to use React with data structures that might be mutable.

With the `useReducer/useState` hooks, the React team explicitly designed them to require immutable updates and pass in new references, otherwise they'll bail out, assuming that since it's the same reference nothing was changed and no render is needed.

Some more details:

- https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-...

- https://beta.reactjs.org/learn/updating-objects-in-state


When updating deeply-nested immutable objects, the Immer library is great. You call the `produce(immutableValue, draft => { ... })` function with some immutable value and a callback function that manipulates a mutable proxy object mimicking the immutable value, and then the function returns a new immutable value with the same changes made by the callback function. The mutable proxy object never escapes the callback, so the use of Immer stays self-contained as an implementation detail of your code without infecting your component's public API or anything.

Another alternative is the "immutability-helper" library, which was originally published by the React team as "react-addons-update". It's a lighter library with less proxy magic going on, but at the cost of being more explicit: instead you create an object describing how to update an immutable value to produce a new immutable value. I've used it in the past in a few places but mostly recommend Immer over it now.


Yeah, we specifically built Redux Toolkit around Immer from the very first prototype that I wrote.

I had catalogued _dozens_ of immutable update libs between 2015-2018, and Immer is simply superior to all of them.

Which is why Immer is a non-negotiable part of RTK, and something we specifically tell everyone they should be using with Redux:

- https://redux.js.org/style-guide/#use-immer-for-writing-immu...

- https://redux-toolkit.js.org/usage/immer-reducers

(in fact, I recently got quoted on the top of the Immer docs with a tweet I wrote saying how awesome Immer is :) https://immerjs.github.io/immer/ )


Where will the Record & Tuple proposal (Stage: 2) fit in?

https://tc39.es/proposal-record-tuple/tutorial/


I'm not sure, tbh.

I agree that having the ability to construct objects/arrays that are compared by nested values, rather than references, will be useful.

But, given that Immer already exists, and only meaningfully requires ES6 Proxy support, I'm not sure how much of an impact the availability of Records will have.

In the case of RTK specifically... Immer is built into `createSlice/createReducer`, and it's _always_ getting called even if you manually create an immutably-updated result yourself. So I don't see any real change or benefit there.


We're using something called Recoil - I'm still getting used to it, but I think it has some similarities.


i dont like proxy magic, its bitten me before. immutability helper ftw


> React has always been designed around some Functional Programming type principles, such as immutable updates

If you think React has anything to do with Functional Programming, please read this ASAP: https://mckoder.medium.com/why-react-is-not-functional-b1ed1...


React was prototyped in Standard ML. Say what you want about whether you think it's functional enough but you can't revise history.


Whether React's implementation is functional or not has no relevance to the question of whether applications written using React are functional.


Where did anyone say "applications written using React are functional"?


> But it is tremendously wasteful to make a complete copy of a large data structure!

You don't make a complete copy: you make a shallow copy of the spine, and then only descend along the fields you actually modify. The number of operations scales as O(branching factor * depth), not O(size).


> You don't make a complete copy: you make a shallow copy of the spine, and then only descend along the fields you actually modify.

Sounds painful.


You can have the computer automate tedious tasks for you.


JS has built-in syntax to do just that. It's in fact much less painful than making a deep copy.


I decided that this was what I would do on my most recent work project. My quest to reduce the number of libraries it depends on led me to deciding against Immer or something like it.

It works out cause I only use state of this complexity in a single spot. The code to do the copying does not read well in my opinion, and it definitely could get nasty in some cases (slower systems with updates on each keystroke say).

A library like Immer seems very reasonable if you are working on big nested structures often. Worth considering depending on the case.


MobX formalizes this way of working with state and makes it non-fragile (and more performant to boot)

I don't think I could recommend doing it the way you describe because there are a huge number of ways it can go wrong



Good article.

What I've found interesting is how many developers think a React "component" (which since hooks is just a function) has some special privileges or abilities that a normal JS function does not. Like, whether it will be selectively executed or what variables are created anew versus reused between subsequent calls. It seems unclear that a React component is just a function, and displays all the behavior expected in a plain old function.

While I agree it was hard to know when React would re-render in the old, class component paradigm, it seems much easier to know when a function will re-render, since it has to re-render whenever the function is called.


It is a bit more complicated in practice though than "a React component is just a function that rerenders when called". In some ways, the function acts more like a class, and then React, internally, uses it to create "instances" of components that have their own set of data stored. (Which is why hooks like useState, useRef, etc. can work - because data is being stored internally in React tied to a component instance.)

It _is_ true that when you call a React function component it "runs its code" just like any regular old JS function. But when that function gets run and what all the side effects of its code are actually is quite complex.


Yeah, this is not to belittle the complexity of React under the hood. But they are functions, and it seems you can assume they will be called in a straightforward manner when they render (whether they are invoked as explicit function calls or via returned JSX).

The only real complexity (for the developer) is the use of hooks, effects etc. if you don't mess with useMemo (which you generally shouldn't). Certainly they aren't pure functions, they have side effects and are stateful, and that has some nuances, but (kudos to the React team) once you understand hooks as a reference to the instance value and a setter for that value, they're pretty easy to understand.

I guess I don't personally find thinking of them as a class as that useful, my mental model of "it's just a function with some external references (via hooks)" gets me there.


> if you don't mess with useMemo (which you generally shouldn't)

why? is this not the primary way to re-init expensive internal component state when specific props change?


GP's claim is a little too strong, but in my experience most uses of `useMemo` / `useCallback` are only necessary because people define things in the wrong scope, or write giant spaghetti components with 15 different props and no internal structure. The best memoization technique is not calling things repeatedly in the first place.


I find it necessary for a lot of different types of patterns that optimize performance. See the very recently written beta react docs, they go over this quite well.

In general, I think it's always a mistake to tell people "don't use this tool because you may shoot yourself in the foot", best to explain the when and why. But I do have a problem when the tool has just stupid defaults that make it harder to use correctly in the first place.


He touches on this in the piece briefly.

> I think as developers, we tend to overestimate how expensive re-renders are. In the case of our Decoration component, re-renders are lightning quick.

Certainly it's there for a reason, and you may have expensive operations, but in my experience developers reach for useMemo much too early and often, and it just adds complexity to their functions. The cost of checking the parameters for changes adds overhead that may be more expensive than just re-doing the "expensive" operation. My rule of thumb is if the operation is less than O(n) where n < ~5000 I don't reach for useMemo.

There have been some benchmarks done on this, and when it pays off to use.


Before you memo, give Before You Memo() [0] a read.

Building on Josh's bit about the UI tree (It's not about the props) [1], just separate the counter and the decoration in to separate sub trees. Doing so leads to nicer component structures anyway.

[0] - https://overreacted.io/before-you-memo

[1] - https://www.joshwcomeau.com/react/why-react-re-renders/#its-...


from the article:

> If a component has a bunch of props and not a lot of descendants, it can actually be slower to check if any of the props have changed compared to re-rendering the component. (I don't have a source for this claim, but I've seen prominent developers like Dan Abramov make this case on Twitter)

It might vary from application to application but this seems like something that could be tested.

Also on the other side of the coin, you have memo all the things: https://attardi.org/why-we-memo-all-the-things/


One funny thing about React is that the most dangerous kind of developer is the one with middle-level knowledge of it

beginners don't know enough to do optimisation seniors know enough that they only optimise what is needed mid-level are capable of build code that needs to be optimised, but they tend to over-optimise and often do the optimisation wrong

for example what a mid-level might do:

const C1 = React.memo(({values}) => {...}) const C2 = () => { return <C1 values={[1,2,3]} /> }

That React.memo is useless because arrays and objects are mutable in JS It also reveals a flaw in JS/React in that memoization often needs to happen at the parent instead of the child, creating awkward code and implicit behaviour and implicit performance traps when forgetting to memoize object/array props that the child expect to be memoized. The child decides which props should be memoized, but doesn't enforce it and no amount of typescript will catch that problem

Fortunately there is a proposal that will fix this problem by introducing records and tuples like python (immutable objects/arrays) https://github.com/tc39/proposal-record-tuple but it still seems far off to being usable as I don't think it will be viable to polyfill it for browsers that don't support it


If you don’t mind a slight tangent, where would you start today to learn front end development with React such that you learn this sort of thing as you go?


The new version of the official docs is currently in Beta and this is really, really good: https://beta.reactjs.org/learn

(In fact, I'll go further than this and add that the section on "Escape Hatches" should be re-read by senior/lead engineers, as many have misconceptions due to learning concepts ad-hoc from code of mixed quality: https://beta.reactjs.org/learn/escape-hatches)


Hmm. The escape hatches page is just “coming soon” for me?


That page is just the "category header" for a bunch of other pages. Look at the sidebar (or on mobile, hamburger menu) to see the actual content.


This is diffused cultural knowledge, but most of the ideas in React can be understood as: view = render(state). It re-renders everything all the time. But practical considerations force some optimizations. Everything else in React follows from those optimizations.

For example, if you destroy and re-create DOM all the time, then it loses critical information like user's cursor position and text selections. It is also slow to read and write to the DOM. Thus the need for an intermediate data structure, the virtual DOM.

React re-renders the virtual DOM every time the state changes. And it then diffs the previous and current ones against each other and does just the minimal number of DOM mutations to sync it up. To speed this up a bit, array elements are denoted with "key" so (I assume) there is a way to see if an element has been added or deleted.

But re-rendering the virtual DOM all the time can also be costly in terms of performance. Thus the next set of optimizations: React.memo+immutable data, and so on..


I've found myself needing to learn React on the go while working on an evolving React codebase, and needing to improve performance significantly and it's been very hard because casually googling for how to do things in React yields lousy hits full of extremely high level "babys first react todo app/blog" type articles.

Absolutely not what I need.

What I want to know is how to build things with React as performant as possible. This discussion of complex and performant apps seems elusive.


The idea is you shouldn't really need to. You have tools to memoize expensive operations in a React-friendly way, but even then you shouldn't really have to think about render cycles.

YMMV but I the only time I've ever had to really think about this stuff was when trying to frankenstein legacy jQuery code into a React app.


In my experience, it's pretty easy to hit performance bottlenecks in React, so I don't think it's that uncommon to have to dive deeper for any reasonably complex codebase. Also, you basically have to understand the React render cycle to effectively use `useEffect` for anything more complex than "do this thing on mount".


I get that, but I personally enjoy understanding the full stack. I built a cpu on breadboard from kit for fun, its just something I enjoy doing.


If you are starting to learn front-end development today, you may question the choice of React.

Consider that a decade ago, people who were starting with the frontend were learning jQuery, which is almost irrelevant now.


>Consider that a decade ago, people who were starting with the frontend were learning jQuery, which is almost irrelevant now.

It's not that simple. At the time, jQuery was absolutely pivotal in bringing about ES5 and the transpilation revolution on the front end. More or less its' entire API was subsumed by the browsers, and so jQuery became unnecessary, but far from irrelevant.

I can see the same thing happening today with React/JSX. There is simply no better way of expressing a UI than JSX-like components. And FRP as a paradigm for UI development is here to stay. So the future of web dev probably looks something like Yew [0].

[0] https://yew.rs/docs/getting-started/build-a-sample-app


If you want a job, learn react


What would they be using instead?


They should probably start with native browser apis for DOM manipulation, and web components. And then explore the current landscape for options that alleviate the pain points discovered while learning (e.g. Lit is nice for declarative reactive components). All the while being conscious of the tradeoffs.


That's a fine idea if you're not expecting the team to grow beyond the original author(s).

It's a pretty terrible idea if you're going to do it in a business setting, as the original author(s) will always be it's Achilles tendon, making the project a liability before it even goes into production.

Most projects use react or angular because it makes onboarding new members easier, and these frameworks really aren't as bad as some people on hn claim.


How much have you researched this space? Do you know of companies that are successfully building their products with web components? Hint: these would include Adobe, Microsoft, RedHat, and GitHub. How do they onboard new members to their terrible setup, one might wonder?


> How do they onboard new members to their terrible setup, one might wonder?

I can't speak for the specific companies you listed, but the number of times I've heard someone sing praises about a homegrown UI framework at their place of employment is approximately zero. The sentiment expressed about those is generally hatred and agony.

That's not to say it can't be done well, but most don't. Also, a company being able to hire/onboard engineers does not imply that their onboarding process is smooth, or that their house-made framework is well designed.


> a homegrown UI framework

I am deeply puzzled by both your and the sibling comment, which suggest that the only way to go is to build a framework. To advance such argument, especially when comparing something to React, is to forget that:

    - React also for a long time was advertised as a view-layer library for creating UI components, not as a "framework".
    - There've been numerous debates in which advocates of Angular or Ember were suggesting that because of such inherent lack of structure, React apps were always different between projects, as opposed to the clear conventions used in Angular or Ember project. This did not deter React supporters and did not prevent React from succeeding.
    - React was created as a library when web browsers did not have a standardized component model; just as jQuery was created as a library when browsers did not have a unified way of interacting with the DOM. Years have passed, and web browsers have matured to the point when a native component model has become a reality. You do not need to home-grow a framework in order to take advantage of them.


Problem is, Web Components still don't support reactivity and passing complex props, so you need a framework anyways, and at that point it might as well be React.

I've seen people do abominations like each web component is a React root, message passing systems on the side for complex objects... better use React directly


> Years have passed, and web browsers have matured to the point when a native component model has become a reality.

This is not true at all, at least not enough to be able to completely replace React (or any frontend framework) with native APIs. You still need some sort of high-level abstraction(s) to tie everything together.


Was going to say the same as sibling @ReadTheLicense, but further Web Components were started before React so the idea that they are more modern isn't true.


Sure, if that's the scale of your project right from the start then creating a new framework from scratch is always an option.

That's the origin of both react(Facebook) and angular(google) after all.


The phrase is Achilles' heel for which the tendon is named.


It does help to have used the basics to understand the benefits of a full framework. But learning things bottom-up isn't suitable for everybody.

As for web components, I'd rather not. Nothing about it works for me. Not the class-based decorator syntax. Not the CSS scoping. Not the use of custom elements. The prop syntax is awful and the paradigm just feels cumbersome.

I work with Angular (its component model is close enough) and have read the Lit docs. Never will I choose that option.

A function is just a better way to write UIs.


Scrimba has pretty great interactive classes too. They specifically have a free React one: https://scrimba.com/allcourses?topic=react

It's quite high quality and the learning environment is great: you're in a live code editor + hear and see the teacher's code/cursor movements.


I can recommend React with Mosh: https://codewithmosh.com/p/mastering-react I completed it and together with the react official documentation (particularly on hooks and newer react 18 features) have learnt enough to build a good interactive application. Building the app immediately after has taught me much more. You can try patching free tutorials together but given the salary paid to good developers, paying for good training is a great investment.

I chose react as I had a large application to make and I knew react had 2 critical libraries I wanted to reuse. Having now learnt react and the underlying ideas, I think Svelte (https://svelte.dev/) may solve the general problem better. However it has less libraries/documentation and community support. If I had more time to learn I would have considered learning it as a potentially superior solution.


What aspects of Svelte would have made it more suitable?


The blog is sooo good! I enjoyed every single paragraph when reading it.

If you're confused about the last part where it mentioned where useMemo and useCallback can help when passing props, I completely understand after reading Kent C Dodds block and Dan Abramov.

Further, I learned a lot by taking interactive course from Dan Abramov in https://justjavascript.com

Highly recommended! good read!!

https://kentcdodds.com/blog/usememo-and-usecallback https://overreacted.io/a-complete-guide-to-useeffect/


To follow up on that, the new beta react docs are largely written by Dan, and they explain the concepts very well, more formally. IMO they're a must-read, even if they're not quite final (some sections missing because they're doing each part carefully one at a time). They're a great refresher and/or confirmation that you're "doing it right", and there's many lessons to be brought into how you approach writing and reviewing components.


Josh's post is excellent!

Related, a couple years back I wrote a post on the same topic: "A (Mostly) Complete Guide to React Rendering Behavior" [0]. It's longer and has more details, but fewer diagrams :) (Josh's ability to make interactive posts is amazing.)

I originally wrote my "Rendering Behavior" post specifically because the existing React docs didn't clearly spell out this kind of behavior, and I was _constantly_ seeing questions that showed people didn't understand these concepts well. For example, many folks assume that "React re-renders my component when its props change", when in fact the real answer is that "React re-renders recursively by default, _regardless_ of whether or not any props changed". There's also a lot of confusion over how Context ends up affecting renders, and I've seen folks end up in situations where setting state in an "context provider" parent component ends up rendering the _entire_ app - not because Context changed, but because they did a `setState()` and that's the natural behavior. So, I was trying to help clarify those sorts of nuances.

I can say that it's been one of the top couple posts I've written in terms of traffic and positive feedback (along with my "Redux vs Context differences" post).

The good news is that the new React beta docs [1] _do_ cover at least some of this rendering info, although it's more contextual in various points of the explanations and in less detail. I'm hopeful that after the main tutorial/API reference material is done and the docs are opened up for external contributions, that we can help add a couple pages that cover some of this rendering behavior info as well.

There's a few other good articles I've also seen covering this topic as well [2] [3] [4].

[0] https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-...

[1] https://beta.reactjs.org

[2] https://www.zhenghao.io/posts/react-rerender

[3] https://alexsidorenko.com/blog/react-render-cheat-sheet/

[4] https://www.developerway.com/posts/react-re-renders-guide


I've been looking for something like your rendering behavior article :) One question though: is it pretty up to date with changes since 2020? (I think there were some things in React 18 that would affect rendering behavior—not sure though).


Heh, unfortunately "update my rendering post to cover React 18" has been on my todo list for _months_ now :)

As in, I literally have a todo list entry that I keep bumping back until "Next Sunday", because other stuff is higher priority. (like, trying to get Redux Toolkit 1.9 wrapped up and shipped... and also playing as much golf as possible while the weather is decent :) )

The shortest answer is that it's still effectively the same - the one major change is that React 18 will now batch _all_ updates in _any_ given event loop tick, regardless of whether those were queued up inside a React event handler or not.

There's a good discussion on this in the React 18 Working Group post on "Automatic Batching":

https://github.com/reactwg/react-18/discussions/21


Ah okay—no problem. Actually the only concrete rendering-related thing I'd seen on React 18 was about the auto-batching, so I'm already clear on the differences there. So I'll still be giving the article a read—thanks for you work on it, and enjoy the golf lol.


> I know some developers believe that every state change in React forces an application-wide render, but this isn't true. Re-renders only affect the component that owns the state + its descendants (if any). The App component, in this example, doesn't have to re-render when the count state variable changes.

Does that mean if i have a redux store attached to my app it rerenders everything when i change something small in the store? Because the store is usually one of the top most HOCs.


No. React-Redux only uses Context to pass down _the Redux store instance_, not _the current state value_. The store instance doesn't change, so that use of context will never cause later re-renders.

Instead, components directly call `store.subscribe()`, and check to see if they need to update after each dispatched action.

See my post "A (Mostly) Complete Guide to React Rendering Behavior" for more details:

- https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-...


No, there is optimization happening at Redux level to ensure components only gets updated when they need to, but it's up to you to ensure your store is properly designed and consumed to avoid unnecessary updates.

Look up Mark Erikson's blog[1], who's a Redux maintainer, for a lot of details on Redux's internals (acemarke on HN), he also answers tons of questions everywhere[2] about Redux, he helped me so much understand it!

[1] https://blog.isquaredsoftware.com/ [2] https://stackoverflow.com/a/40386189


Thanks, glad to hear that info's been helpful! :)


No. The redux <Provider> component is just a simple React context that uses `children` [1]:

<Context.Provider value={contextValue}>{children}</Context.Provider>

And `children` behave diferently. `children` is a prop and thus not considered as an element of the Provider component, but of the App component [2]. Meaning the components inside `children` will rerender when App renders, and not when Provider renders.

1: https://github.com/reduxjs/react-redux/blob/master/src/compo...

2: https://www.developerway.com/posts/react-elements-children-p...


The opposite of Big Misconception #1 is also common. People who think React never re-render anything besides very specific areas that needs to be updated.


Don’t browsers already have similar update logic implemented in their HTML DOM?

If the DOM tree stays but a property of some element changes, if that property affects size or layout or other visual state of things, the browser will re-render the changed element, along with any dependent elements. Could be many elements when re-layout is needed, could be just one if the only change is color.


My brain reads the title as "why not calling `render()` manually?".

Probably unpopular opinion: manually call `render` is always better.

Creating your own `ui = f(state)` is very trivial with DOM event, custom element, and a whatever 5kb pure view lib. With event delegation technique and a global store, I can build almost whatever ui with this pattern.


It's both. You should be able to control it, but can have a same default that does it for you automatically under certain conditions. Unfortunately React forces the latter only


Yes, practically force re-render comes with unnecessary mechanism to prevent it from happening, and there are unimaginable cases of state mutation that shouldn't force re-render. Example situation: changing state A, B and C, re-rendering, changing B again, not rendering. Auto-render + prevention mechanism is inferior means compared to just having `render` and just calling it whenever and wherever.


I sometimes wonder what people are using React for, that they wouldn’t know this or have figured it out along the way. Let me kind of explain, as best I can, without code.

We have a complex software product, an integrated compliance and risk management system with embedded workflow, automatic highlighting of potential risks due to non-compliance, plans of actions (aka risk management plans), RBAC, ABAC (used to control different things), etc.

The backend does most of the work. What gets presented at the frontend can be complex.

The React component model gives us an almost functional DSL that we use for compact, highly expressive tooling that allows us to integrate common look and feel, table exports, etc., across some really complex data.

We started before Hooks were widely available or widely known, and, we started with class-based components, because we realized pretty quickly we were going to have to do some funky state management, especially where workflow was involved (we want a common look and feel across processes and tasks, so workflow tasks and processes get wrapped in higher level components that call back to other high level components; the forms can have dozens or hundreds of variables, some interdependent, so state has to be communicated up for validation; React handles sending it down).

Our key state management function is a custom signal handler that gets passed as a prop to all sub components, then, via a common wrapper, back up to the high level components that group everything for display and consistency purposes.

As we refined this handler (and a few others), we moved from class-based to functional components.

The functional components are simpler than the class-based components they replaced, with much of the complexity located in one place, the handler.

Our handler allows us to pass state up “just far enough”, and React handles updating all affected components.

Performance is fantastic, validation is easy(ish; it takes some staring to grok how it hangs together), and debugging (once one has grokked, is straightforward(ish; there are edge cases that cause pause, until the “oh, yeah, that” moment).

That functional DSL has allowed us to build several suited-for-purpose DSLs, e.g., our workflow system, which, while not no code, is low code (configuration as code more than anything else).

If we didn’t understand the React state management model, none of this would have been possible.

So I ask, in naïveté, what are people building that they didn’t need to know that?

(We are likely to move to Hooks anytime soon, because we’ve already solved the problem they were introduced for, and without having to rewrite much. Hooks look like we would have to make wholesale changes, in which we see little value, at least right now, OMMV in the future).


> I sometimes wonder what people are using React for, that they wouldn’t know this or have figured it out along the way.

The article is for beginners (the article itself says its intended audience beginner-intermediate, but I'd say it leans very much towards beginner). Thus this is precisely an example of how people who use React would learn this. It would be odd to comment on every explanation of a concept that everyone would have already learned that concept.


Ah, OK, fair enough. For an article intended for beginners, well, it made think of those recipe sites that bury the details after autobiographical recollection.

It could have been ever so much shorter and clearer. But I’ve realized I really am becoming a get offa my lawn type as my 60s near.

Ah, Usenix and the early web, I miss thee. ;-)


You can get away with a lack of understanding about these things and still create beautiful products. For an example just look at Josh’s website. Beautiful react and design execution without knowing these details about rerendering.


Fair point. Speaks to the utility and power of React.


Several things can happen when you don't understand this re-render process exactly right, among them:

- slow UIs which re-render too much. For instance, re-render of a big chunk of the component tree for each key press because you somehow found it was a good idea to pass the new value all the way up to the root of the component tree without realizing that it will re-render everything (because React will only re-render what actually changes, right?). Or, if you understand it, trusting too much the browser on its ability to run this fast. Of course, if your workstation is quite beefy and you work with small data just to test if it works during development, and are used to slowness from your OS, you may not notice the issue at all until a user with a regular computer attempts to write a bigger text than what you ever tested on your beefy machine. And of course React pushes for managed components; performance, memory usage and GC pressure be damned.

- "a second ago" "helpfully human-friendly" indicators that stay "a second ago" forever, because nothing ensures a re-render when necessary, or because the wrong value is passed / stored.

- jumps and erratic scroll behavior (on unsuspected/non-ununderstood so seemingly random re-renders), because you are building the UI declaratively but some stuff actually need procedural solutions, and doing this in React can be difficult, and you end up with difficult to understand buggy workarounds. Think adding a new blog post at the top of a blog post list that the user is currently reading, or a new message that gets added at the bottom of a message list. Yes, I know, CSS anchors are supposed to solve this, but no, you can't always use that, by the way they are still unsupported by Safari. The fact that everything is asynchronous does not help at all by the way, you can't just position things right after you add them to the DOM and right before the next visible paint like you could with vanilla JS.

I'm not making these things out off my ass.

React is sold as something that makes the lives of frontend developers easier, and many frontend developers find it compelling because of this, but in reality it's way more complex and add way more complexity than probably many people suspect / notice. Especially if you add Redux, which is surprisingly action-oriented in this declarative world - quite jarring, and also quite complicated to understand (just let me increment this counter or add this element to this list already, I don't want to deal with these freaking "reducers"! Why can't I deal with these values like in the rest of the React app, by using some kind of setState function?). Svelte got this right. The store is reactive and not action-based, very easy to understand. Also, why such an essential feature isn't provided by React itself?

React is cute and give the impression to help manage complex UIs for which vanilla JS would supposedly be a mess, but if it is not perfectly mastered, I'm yet to be convinced that this is true. You need to have a really good intuition on how React works to design a complex UI with it, probably to the point to be able to write a small prototype of React.

The reality is that React is hard to understand, but it's easy to not notice this.


Sound pretty interesting! We’ve also implemented a DSL on top of React, see Lowdefy [0]

We’ve taken a different approach, we’ve written a pure js engine which computes and manages state based on operator used to express logic, and then have a recursive render loop in react which provives engine with update hooks to it uses to rerender components when it should. That way we can very handle complex state logic and then update with ease all without dealing with passing state up and down.

We still have a few ideas on how to further optimize which will be built in future versions. But already we, and the OS community are building some advanced apps using Lowdefy

[0] - https://github.com/lowdefy/lowdefy


I like the sounds of that. Every now and again we look at alternatives to our implementation: it works, so we don’t want to break it, but hic sunt draconis and caveat programmator.

I’ll be checking out that repo, thanks!


You could have achieved the same thing with Mobx stores without having to pass a handler around everywhere. Although it sounds you're using context-dependent state that don't need to exist globally. Well, it would still work and you'd only need to know a couple of Mobx pitfalls in order to make it render efficiently. So no, you don't need to understand React state management model completely in order to build working apps.


Can someone tell me why this page needs 13MB of resources to display?


It's even more for me, it looks to me like the embedded React sandboxes are essentially full React apps in development mode. So none of the usual optimizations for size are active, and you get the full devel-mode bundle for several React applications on this page.


My guess is for the code editors and associated transpiler. The demos allow you to edit the code in JSX, requiring the tooling. Kinda impressive imo.


This page doesn't know if it is an app or blog post then! Maybe it should lazy load those resources?


Here are the top requests by file size from the page, screenshotted since I couldn't easily copy/paste: https://imgur.com/N1ANEIb

Would love to better understand why the site is pulling in the Babel transpiler in production?

Edit: siblings know more than me, thank you siblings!


Now that is how you start a flame war in the most subtle way possible.


When I checked it was 46MB! Looking at the network tab, it seems to be making a bunch of extra requests for the same assets.

I know that Vercel/Next.js will lazily load in certain JS chunks but there seems to be some mechanism that is loading a ton of stuff up front. Edit: from other comments in this thread, it's the sandbox code, really crazy how much overhead that adds to the page!


I need to finally buy his CSS course - all the details in his pages and materials are just gorgeous.


JS works in mysterious ways.


I think that is the point, is that it does not. It is code, and it does what it is told. But it is quite possible to be a professional dev without fully understanding exactly how and why your frameworks doing their thing, so it feels hand-wavy and mysterious.


I'm among the React developers who don't really know how it works under the hood. Recently I needed to code a UI that required mouse drag events. It ran like a pig when I did it in React.

I tried a few things to speed it up, but eventually gave up and did the UI in plain old Javascript. It runs a hell of a lot better, but the code does feel a lot more flimsy.

Edit: Here's a demo of the UI I built https://www.youtube.com/watch?v=Wt71bNYe3qc


Draggable UI (w/o libraries) is one of the places I enjoy React most, maybe re-rendering wasn't to blame. Might have been an issue under the hood (as drag fires multiple times a second)? From the article:

> I think as developers, we tend to overestimate how expensive re-renders are. In the case of our [pure] component, re-renders are lightning quick.

> If a component has a bunch of props and not a lot of descendants, it can actually be slower to check if any of the props have changed compared to re-rendering the component.


I'm no expert, but mouse drag events in React are fairly simple to get working performantly, even without understanding how it works under the hood. There are even many libraries that provide functionality, all without it running 'like a pig'.


Also:

> Can pigs run fast? Domestic pigs can run as fast as 17 km/h while wild pigs can reach a speed of 30 km/h!


Yes, but I didn't want to use a library because I was doing something a bit out of the usual case.

I do like understanding my code as much as possible. So I was choosing between: 1. Understand React better, and reading about the "React" way to use these mouse events. 2. Doing it in VanillaJS


My point was not to use a library, but that many others have implemented functionality in libraries without performance decreases. Granted, your use case may be special to the point where vanilla JS is better, but given how many libraries are out there, as well as how many may simply be poorly implemented yet still work fast, makes me wonder what you were indeed doing.


>I do like understanding my code as much as possible. So I was choosing between: 1. Understand React better, and reading about the "React" way to use these mouse events. 2. Doing it in VanillaJS

The great thing about the "React" way of doing things is that it's just the JavaScript way of doing things.

React can be summed up entirely as: "a function that takes in props and returns rendered HTML". It is not a framework. There is no black magic. There are no idioms. There are no batteries included.

Anything else you do with it beyond that is entirely up to you.


That’s a bit too strong imo. There is magic, and it can generally be found in implicit re-render conditions.


"Rules of hooks" is a thing.


It's not unlikely that you missed some hints of the latter part of the article (JS equality for functions or objects) although it's difficult to say without seeing the code.


There is nothing about react that requires you to understand "how it works under the hood" to use mouse events in react. You just need to sit down and read the tutorials and learn to use react properly. Try the new beta docs.


True, but I'm quite happy using VanillaJS. I'd like to avoid React as much as possible in the future.


You seem to be conflating javascript and react here, and even if you weren't, the entire point of this article is that nothing works in mysterious ways.


This was fantastically written! Thank you.


Because hooks 67,48,47, and 51 changed.

Edit: Sorry, because the parent component re-rendered too. Except it didn't. But maybe? Nah, it didn't. Did it?

A lot of people are going to think this is being ridiculous, but there were actually bugs as recently as a few months ago(maybe still?) in the dev tools where they would not know the actual re-render reason and it would spit out a generic "parent component re-rendered" instead.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: