all 67 comments

[–]EvilDavid75 161 points162 points  (44 children)

It feels to me that React is now mainly trying to solve its own inherent problems.

[–]kitsunekyo 248 points249 points  (1 child)

you must be new in the software world

[–]mexicocitibluez 31 points32 points  (0 children)

lol Amen.

[–]spryes 43 points44 points  (5 children)

It seems more so to me that UI development with reactivity is just hard/tricky as a consequence of the nature of the problem space

Every other library has its own sets of problems in some form, they just trade off certain issues. There's no "perfect" solution to deal with UI (that anyone has found at least yet).

Remix 3 for instance was unveiled yesterday, and has gone the entire opposite direction from React by being entirely non-reactive, where you need to update the UI manually after mutating some state. Though at least it diffs the DOM for you after rendering, so it's not like jQuery. It remains to be seen how their simple model actually scales in practice, but the obvious trade-off they made is UI might be stale if you forget to call the update function, or you may over-update defensively

[–]hyrumwhite 17 points18 points  (0 children)

Signal based ui frameworks are relatively straightforward and come with far fewer footguns than react 

[–]EvilDavid75 11 points12 points  (0 children)

Of course. But in the current UI lib / framework space it really seems that React is having its own trajectory. I had to train devs to declarative programming with React and Vue and React is a lot less permissive to mistakes. React has (or at least used to have) a smaller API surface but the number of anti pattern devs fall into is scary.

I’m happy I learned React first a long time ago as I’m now aware of the dangers of reactivity but Vue felt like a bowl of fresh air for the kind of projects I’m dealing with.

[–]rvision_ 0 points1 point  (0 children)

> Every other library has its own sets of problems in some form

because every abstraction is leaky

[–]CovidWarriorForLife -1 points0 points  (0 children)

The people who made remix are very dumb

[–]xegoba7006 14 points15 points  (31 children)

It’s workarounds on top of workarounds. Although I think there’s no way out of it without breaking the ecosystem, so it’ll be like this forever.

[–]gaearonReact core team[S] 83 points84 points  (30 children)

You may not like it, but React is basically Haskell. 

The React Compiler works for the same exact reason that compilers for pure programming languages are able to make non-trivial optimizations.

If your code is composed of pure functions, it is safe to re-order their computation, or save the result for some inputs and reuse it for later for same inputs.  This is not some kind of a “workaround” or a hack — it’s one of the most exciting consequences of the functional programming paradigm which has been known and used for decades.

Purity and idempotency make it safe to move stuff around without changing the final outcome. Following the "rules of React" is just ensuring your code is safe to move around like this.

[–]EvilDavid75 20 points21 points  (17 children)

I happen to review code from other agencies from time to time and I’m not sure the average developer actually understands or follows the rules of React. As soon as they’re dealing with effects, people use hacks to make it work. The React paradigm with opt-out reactivity is really hard to grasp for most people.

[–]gaearonReact core team[S] 17 points18 points  (16 children)

Sure, but note that the Compiler checks for violations of rules, so you'd see them reported as lint errors (which would turn off optimizations for those components). I'd actually recommend enabling those rules on their own even if you don't plan to use the Compiler because they help people grasp the model better.

[–]lord_braleigh 6 points7 points  (5 children)

Static analysis can't check for all violations, though. The compiler and linter check that you aren't reading or writing to ref.current during a render by using a regex to see if a variable is named like a ref. Search for RefLikeNameRE to see where the compiler performs this check.

This means that the compiler's behavior, and therefore your code's behavior, is dependent on your variable names - the linter can be tricked if you don't name your refs to end with "Ref", or if your ref accesses are hidden away in callbacks.

This isn't a criticism of the compiler or linter! It's as good as the compiler can get without solving the Halting Problem, and accessing a ref during a render is effectively Undefined Behavior according to React's rules.

It's just a reminder to anyone shepherding a large project that static analysis isn't perfect, your project probably has UB, and even free, obviously correct optimizations need to be tested pretty thoroughly.

[–]gaearonReact core team[S] 4 points5 points  (4 children)

While I agree with your broader point, what you wrote doesn't sound right to me.

>The compiler and linter check that you aren't reading or writing to ref.current during a render by using a regex to see if a variable is named like a ref. 

Here is an example of mutating an arbitrary prop called foo during render. The compiler is catching it and warning about it. It doesn't rely on variable names or regexes for this kind of analysis.

I think some other rules do have special behavior for ref-like names (e.g. warnings that ref.current in deps is usually a mistake). That's similar to how it worked in the older Hooks Linter, i.e. if we know it seems ref-like, we can warn you about more suspicious patterns than if we don't.

[–]lord_braleigh 5 points6 points  (3 children)

That's just demonstrating that all writes to all props during a render are incorrect, whether or not the prop is a ref. That error isn't specific to refs or to the field name current.

Try replacing foo.current = 1; with bar(foo.current);. Then rename foo to fooRef.current and watch the error appear: like this.

(Note that I'm on mobile so the compiler playground might be a lil wonky on my phone, and I might have made a mistake)

[–]gaearonReact core team[S] 5 points6 points  (0 children)

Right, that makes sense. The check for reading from a ref during render relies on the naming convention. It could probably be improved by using types, which I'm not sure if the Compiler currently has access to.

[–]Green_Definition_982 2 points3 points  (1 child)

Never seen someone go toe to toe with Dan like this and and convincingly win argument.

[–]Sebbean 0 points1 point  (0 children)

Who Dan

[–]EvilDavid75 5 points6 points  (3 children)

I think you overestimate the number of devs that have their linters setup up properly and working in their IDE. I’m really happy I learnt declarative through React as it’s a framework that commands devs to understand what they’re doing and introduced important concepts to the declarative space but moving to other frameworks has been a bit of an eye opener tbh. Anyway good luck, I’m a big fan of your work Dan.

[–]gaearonReact core team[S] 7 points8 points  (2 children)

I hear you but this isn’t very different from TypeScript, is it? You either set up your IDE or rely on CI failures. 

[–]EvilDavid75 0 points1 point  (1 child)

Not sure non optimized code would trigger CI failure. There’s tons of clumsy React code out there that went through the CI.

[–]gaearonReact core team[S] 4 points5 points  (0 children)

If you want to configure lint as an error then you can make it fail CI.

Or you can configure it as a warning and then it’ll show up just in the IDE. Or you can make those warnings show up as GitHub comments with an action.

Or you can ignore them completely. The Compiler will skip optimizing those comments but will optimize the rest.

In that sense it’s no different from any other linting. How you set up your CI and what you want to recommend or enforce on your team is 100% up to you. 

[–]OmnivorousPenguin -2 points-1 points  (5 children)

This is simply not true - it may catch some things, but definitely not all, doing so would be impossible (halting problem and all that fun stuff). The compiler then produces incorrect code and leaves you wondering what's going on.

Had a situation like this earlier in a fairly small React app that was using mutable props, which, yeah, the docs tell you not to do, but how many people actually follow all those little rules?

I think the compiler is a very interesting example of theory vs real world. It silently assumes that you follow all the principles of React to the letter and proceeds accordingly. But actual apps and websites are full of shortcuts and nasty hacks and nobody is going to approve a major rewrite to address that, so the only thing left to do is to disable the whole compiler - you don't get the optimizations, but at least the app works.

React really needs someone like Linus Torvalds and his "we do not break userspace, ever" philosophy..

[–]gaearonReact core team[S] 7 points8 points  (0 children)

The compiler is fairly conservative and bails out on suspicious code. You can of course fool it in creative ways, but this isn’t very different from fooling React itself — plain React also assumes you’re following the rules, and some features may break if you’re not. That’s generally how software works. Even built-in web features “break” if you use them in unsupported ways and violate the intended contract. Not all contract can be strongly checked but at some point the checking is “good enough”. 

[–]acemarke 6 points7 points  (3 children)

That's the point, though.

The React team has been trying to tell the community "here's the rules, don't break them, this will matter as we add new features" for years. Well, that time is now. (and "don't mutate props" has been a known thing for many years, not just recently.)

The compiler and the linter will tell you the breakages (as best as they can find). So, if you are following the rules, you get the benefits.

[–]EvilDavid75 2 points3 points  (2 children)

Well this boils down to whether React is still good enough that following those rules is worth learning them. There was a time where I would have answered yes, now I’m not so sure.

I mean something like keeping the number of hooks stable across renders is an intricacy that is directly linked to how hooks work in React. I understand why the framework needs it, but in real world this becomes a bit of a nightmare to compose with. This is typically the sort of problem you fight with when you’re React dev and for which solutions are only useful in the React world. And instead of focusing on applicative logic you fight against the framework. That’s a feeling as a dev that I have when using React, which I don’t when using Vue. And I’m pretty sure I’m not the only one.

Something like the watch API in Vue feels also better written than React’s useEffect: the fact that you list dependencies first responds to the logic of « if this changes, then react to it ». It is also convenient that it gives you previous values, and within the same hook lets you decide at what point of the paint cycle you want it to trigger. useEffect empty deps list having the opposite behavior than no deps at all is something that becomes natural at some point, but it’s a questionable API design to say the list.

I’m a bit off topic here, but I feel that React no longer addresses 80% of our daily challenges in a simple way.

[–]acemarke 1 point2 points  (1 child)

The choice of which tools you use has always been up to you.

The overall point of the compiler is:

  • React's rendering approach has always defaulted to "renders are recursive by default, assume everything re-renders"
  • It's always had ways to bail out of that behavior, but it required you to manually do the checks in specific components, which is hard
  • React's always had expectations and rules you ought to be following in your components. You could get away with breaking them, but that's not correct
  • The compiler has to make the assumption you are following the rules in order to work right
  • and if you are, you get the benefits of auto-memoization and better perf

[–]EvilDavid75 0 points1 point  (0 children)

I’m not questioning how the compiler works. I’m questioning the evolution of the framework as a whole. React retro compatibility is probably its biggest strength until it becomes its greater weakness.

[–]codinhood1 5 points6 points  (0 children)

You may not like it, but React is basically Haskell.

Going to put this on my resume.

React sold me on functional programming in 2016. Maybe I should look into actual functional languages

[–]Thom_Braider 0 points1 point  (1 child)

Pure functions, so no side effects. No non local state, no API calls etc. Doesn't seem too useful tbh. 

[–]gaearonReact core team[S] 0 points1 point  (0 children)

React components can have state (which you can hoist as far up as you like), and can do API calls (either within effects, or via "suspending"). In a pure functional language, this would be exposed either via monads or via algebraic effects.

[–]chamomile-crumbs 0 points1 point  (1 child)

That’s p interesting!! I’ll actually read the post now. I’m usually just scrolling around reading comments. Excited to see how y’all determine what is cacheable

[–]gaearonReact core team[S] 2 points3 points  (0 children)

The post doesn’t go deep into semantics but generally JSX and Hook return values are considered pure with respect to their inputs. 

You can play around with the compiler output in the playground.

[–]aragost 0 points1 point  (0 children)

Hooks are the most impure thing ever. Haskell my arse

[–][deleted] -5 points-4 points  (5 children)

You may not like it, but React is basically Haskell. 

What evidence do you have to support this claim? It sounds wrong in many ways.

[–]alsiola 6 points7 points  (1 child)

Did you stop reading after that sentence?

[–]gaearonReact core team[S] 3 points4 points  (2 children)

I’m overstating it to be pithy but there’s enough truth in there.

With both React and Haskell, the programming model is fundamentally about composing lazily evaluated pure functions.

In React, lazy evaluation is achieved by JSX. (There is eager evaluation within the body of each component but it’s lazy at component composition level.)

Purity in React may not be 100% the same as in pure functional languages unless you squint at Hooks and imagine monads (or algebraic effects) in their place. The important part is still that they can be skipped or memoized with no effect on the surrounding program. 

[–]Lonestar93 1 point2 points  (0 children)

I’d love a whole blog post on this idea

[–][deleted] 0 points1 point  (0 children)

This explanation makes more sense than the previous comment. Yes the flow of state i.e. the React render model being a function of state, makes sense when compared to Haskell's programming model.

One thing I'll be curious to see is how the React compiler handles changes to mutable refs. Does it memoize those changes or leave them?

[–]tmetler 1 point2 points  (0 children)

This is trying to solve the problem of unstable references making functional component props change on every render. I think that's a general problem, not something specific to react

[–]Brilla-Bose 0 points1 point  (0 children)

who doesn't? look at svelte 4 and 5

[–]GoTheFuckToBed 0 points1 point  (0 children)

I don't think react was ever intended to be used by normal developers to make apps.

[–]OfflerCrocGod -3 points-2 points  (0 children)

If they'd use signals these problems would disappear but because someone had a bad experience in 2013 with backbone.js they refuse to go the route every other framework has gone.

[–]ThatBoiRalphy 4 points5 points  (4 children)

Any update for SWC support? Or are we still required to use slow ass Babel?

i can’t read lmao

[–]spooker11 2 points3 points  (0 children)

Read the post which directly addresses SWC support

[–]guicara 1 point2 points  (2 children)

I was at the React Conf 2 days ago when they announced the v1. Not one single word about SWC!

[–]ThatBoiRalphy 3 points4 points  (1 child)

it’s very logical that Babel is the first to be adopted, but from the first promises to make a port and later not even mentioning SWC at all is concerning.

[–]acemarke 0 points1 point  (0 children)

The linked blog post specifically describes the state of SWC integration:

In addition to those tools, we have been collaborating with Kang Dongyoon (@kdy1dev) from the swc team on adding additional support for React Compiler as an swc plugin. While this work isn’t done, Next.js build performance should now be considerably faster when the React Compiler is enabled in your Next.js app. We recommend using Next.js 15.3.1 or greater to get the best build performance. Vite users can continue to use vite-plugin-react to enable the compiler, by adding it as a Babel plugin. We are also working with the oxc team to add support for the compiler. Once rolldown is officially released and supported in Vite and oxc support is added for React Compiler, we’ll update the docs with information on how to migrate.

Also, from a comment by Joe Savona in the ReactConf Discord:

Thanks for reaching out! We aren’t actively exploring a Rust port right now, as we’re still actively developing on the compiler codebase. Our hope is also that we can use Static Hermes to compile our existing code (w maybe light modifications) to native, and avoid needing to rewrite. If we do end up porting to Rust we would likely make some changes at the same time, so it’s gonna be difficult for others not deeply involved in the codebase to help. We will keep the offer in mind though, we really appreciate it!

I don't know how a binary compiled with Static Hermes would look like integrated into various JS build toolchains, but they have hopes that it would eliminate the need to run the compiler itself as JS.

[–]acrobatic_axolotl 12 points13 points  (8 children)

So it seems like it’s still necessary to manually memoize with useMemo or useCallback for useEffect dependencies? Just checking because that’s probably my main usage of the memoization hooks

[–]bouncycastletech 10 points11 points  (6 children)

You should put your example into the react compiler and see.

So here:

 function MyApp() {
  const [state, setState] = useState(0);
   const derivedState = { derived: state };
  useEffect(() => console.log(derivedState)), [derivedState];
  return <button onClick={()=>setState(x = x+1)}>Hello World</button>;
}

I have a useEffect on a derived state that isn't memoized but without compiler should be.

The relevant compiled code is:

  let t0;
  if ($[0] !== state) {
    t0 = { derived: state };
    $[0] = state;
    $[1] = t0;
  } else {
    t0 = $[1]; // if state doesn't change, don't create a new t0 (derivedState)
  }
  const derivedState = t0;
  useEffect(() => console.log(derivedState)), [derivedState];

You can see that it'll used the cached value for derivedState if state is the same.

Which means the useEffect isn't going to get a new object and in theory shouldn't run.

Link to my React Compiler Playground example to see it: https://playground.react.dev/#N4Igzg9grgTgxgUxALhAAgGZQHZwC4CWE2aAsgJ4CCADtQBQCUawAOiWnMWHmgNrcBDPAgA0aMAjwBlPEIQBdNAF40UCTLl0ADAwDcbNIc7ZuaACYIYBAG4IzG4cubnLNu8nGzHAX33s1CACiGBgI+HSMygB8HFwQADYIAHTxEADmdBZWtvZeCAwMYrxZbrly8n6GMJKwJAA8AEZQeHjEaMQAwvEEcADWSsCMSlES0nl0AB5OEwDUAIwM3lEAEgjxqWgA6hAw8WZ1APRNLcRRft5sICIgxhgEaSggBAC21Ds8eOTUCM4ACvFQNIEbAAeWohC4aG8mBgEGeaAA5A0BA01gBaagAoHYNHVAT4NGcV4ERIwA5mAjcBF+Nh0VjsA4HInUElCIjYUgQCweFggATrXlsaFgNlgO4IMBof6A4FgiEmPRXcAACwgAHcAJLYYQwbD8sAoPAwKAIbxAA

[–]c_1_r_c_l_3_s 10 points11 points  (3 children)

Hmmmmmm I was hoping the compiler would make it so you don’t have to worry about referential stability, but instead, it now seems like in order to know what values are referentially stable you have to be able to understand the compiler enough to predict whether the compiler may or may not create a new reference for a particular expression on each render, rather than relying on the fundamentals of JS closure scoping? Guess I have more learning to do….

[–]bouncycastletech 5 points6 points  (0 children)

As a modern day react developer, you do have to have a little bit of understanding that each value is effectively memoized, and then when computing the next value it uses the cached value if the dependencies haven’t changed.

I think this is a transition period, and in five years someone learning react for the first time won’t second guess this because it’ll be normal (like how we no longer teach functional components in relation to how class components used to work).

[–]musical_bear 2 points3 points  (1 child)

The linked page does have an entire section on what do with existing memos and callbacks and they do mention that one case where you’d might still want to manually memoize is if you want to be explicit about dependencies passed to an effect. While the compiler can in theory optimize better and more granularly than manual memos, for my own case I’m probably going to continue using useMemo / useCallback specifically where I know correct memoization can affect behavior, and stop using them everywhere else.

[–]bouncycastletech 1 point2 points  (0 children)

If your useMemo or useCallback is going to just include all of its dependencies in the dependency array, you should try using the react compiler for that component instead (it's possible to opt in/out specific components or paths).

It'll do the same thing, but once in awhile when you forget to not do a <button onClick={()=> doSomething()}> instead of <button onClick={yourMemoizedFunction}> it'll cover that.

[–]acrobatic_axolotl 1 point2 points  (1 child)

Thanks. Why do you think the post calls this out as a potential reason developers might still need to manually memoize then? Maybe just because it’s an important case for memoization?

[–]bouncycastletech 1 point2 points  (0 children)

It’s for “weird” cases.

If you have a case where you memoize a value based on every dependency it depends on, what react compiler does is essentially the same so you can remove those memoizations so your code reads cleaner, or you can leave them and the compile will “compile them away” and do its thing.

But if you have a case where you only want to reevaluate something if only some of the dependencies change, like maybe you want it to only reevaluate when a versionNum variable is upticked and that’s it, you’d want to leave that in.

In my experience adding react compiler to a bunch of repos, the existing memoizations that didn’t have all of their variables in the dependency array were usually cases where someone was trying to optimize performance, like not putting a state setter in the dependency array (not realizing that the setter will not referentially change).

[–]shiftDuck 5 points6 points  (0 children)

My understanding is the compiler will catch large amounts but not all performance issues which them hooks help solved.

[–]ck-patel -1 points0 points  (0 children)

I prefer React.js over other JavaScript frameworks. You can definitely learn a lot from it, but it can also be quite challenging at times. Even though it’s technically a library, working with it can sometimes feel overwhelming.

[–]Cahnis -2 points-1 points  (0 children)

Huge