Hacker News new | past | comments | ask | show | jobs | submit login

It’s kind of funny how cryptic react can be. Very easy to miss that having an empty [] as a second parameter will result in completely different functionality.

React is pretty nice overall, just why do we design things to be so unintuitive. Dark patterns.




The irony is that the whole sell for hooks were that they were easier than classes, which people argued were too hard to reason with. There has been so much hype for how much easier things are with hooks and yet here we are.


I agree it's not easier, but I thought the main argument was better composability. And I think that it achieves that goal quite well.


Hooks allow functional components to be used for non pure components.

Which is a huge win IMO because the whole class/object paradigm in JavaScript is broken, and tracking what ‘this’ might be is literally impossible.

I think that’s sufficient reason to use hooks.

Hooks bring a completely new paradigm and I think it was brought out of Beta too early, so React now has to stick with certain concepts that appear bad ideas in hindsight (a fairly simple and relevant example would be that running useEffect on every render should not have been the default…I suspect the React team could have swapped the behaviors for empty array dependency and no dependency parameter, and it wouldn’t be any more logically weird than what we have today and the default behavior would have been far less footgun-ny).

I do think the general idea behind hooks is excellent. I think some of the existing choices, however, need a significant rethink, with the additional real world experience the React devs have with it now.


> Which is a huge win IMO because the whole class/object paradigm in JavaScript is broken, and tracking what ‘this’ might be is literally impossible.

I chuckled. `this` is a piece of cake to understand compared to hooks.

`this` is a function argument that is typically passed to the function by placing it left of the dot at the time when you call the function. That's all you need to know - now you understand `this`.

Hooks are a whole nightmare in comparison - there is a stateful counter that assigns an index to every `useState` call just to determine which result you need to get back. This is a terribly error-prone design that passed review with lots of dubious justifications.


There is a fifth dimension beyond that which is known to man. It is a dimension as vast as space and as timeless as infinity. It is the middle ground between light and shadow, between science and superstition, and it lies between the pit of man's fears and the summit of his knowledge. This is the dimension of imagination. It is an area which we call Hook Hell.


I've seen people reference that "`this` keyword is confusing" argument it has to be something people picked up at a conference or on twitter and just repeat.

And if they really wanna go there, it was the React team itself that took away method auto-binding when they forced everyone to use `class` keyword. Mixins were already available in `createClass`, and if they wanted privacy between mixins, that's totally do-able with Symbols.


Always use myClassMethod = () => { ... } style syntax in classes, and stop worrying about 'this'


With hooks we have now to dialects of the same framework. It is so confusing some time!


You can make it a linting error. There’s a lot of functions that can behave wildly different in very subtle ways. I always enjoy quizzing people on what breaks about this and why it’s not obvious:

    myStringNumbers.map(parseInt)
Because of this I find it very smart to tool up with opinionated linting rules + TypeScript. Won’t catch everything but it covers a lot of easy mistakes that exist everywhere.


Yikes, I had to look up why that doesn't work as intended. For others, map passes the value, index, and full array to a function, which is a noop for a fn of 1 argument, but parseInt takes a second argument, the radix to parse. So each iteration actually calls parseInt(value, index), which gives unexpected values.


IMO this is a more fundamental issue with 'map'. A separate indexed map would make sense because you expect functions passed to map to take one parameter in almost all cases and usually when you need indices you should just use a loop.


Some languages use `enumerate` for this. Maps, filters, for loops etc all work on single items, but enumerate wraps them into (index, item) tuples when needed.

https://docs.python.org/3/library/functions.html#enumerate

https://doc.rust-lang.org/std/iter/trait.Iterator.html#metho...

https://docs.julialang.org/en/v1/base/iterators/#Base.Iterat...

Amusingly in Haskell it only takes nine characters to define enumerate, so there’s not much benefit giving it a name!

    enumerate = zip [0..]
https://stackoverflow.com/a/6473153/119271


I agree with you. I ran into that issue once. Of course I knew map takes a function that it passes value, index, array, I just forgot for a moment. It would arguably be better to have different functions for those.

Very tangentially related, Apple's Metal API has a function that copies a texture. You pass it a width, height, etc... But, if the texture is compressed, then 255 of 256 possible value combos you pass it will be invalid since compressed textures can only be copied in block multiples. I think it would have been a better designed function if it only took width and height in blocks instead of pixels (with uncompressed textures defined has having 1x1 pixel blocks). Then this nonsense of 255 of 256 values being bad would disappear. There's a ton of other inconsistencies in that function. For example, you pass it destinationBytesPerRow when copying to a buffer but if the texture is compressed you pass it say 40 rows and it will only only actually copy 10 rows of blocks and only advance the destination every 4 rows instead of every row. It's arguably a poorly designed function. Thought, I suspect it was inspired by similarly poorly designed functions in other graphics APIs


I can’t speak to Metal, but you’ve gotten at what I wanted to be contrary about in my sibling comment: number (or equivalent, and same goes for other primitives) is an inadequate type. If it’s an array index, it’s not just any number. If it’s a range restricted count of something like pixels, it’s not just any number! Any code in almost any language can box these types so they’re safe to use, but almost no one ever does except in ML langs (or maybe Javaish ones) because across the board it’s a huge bunch of ceremony for mostly worse performance.


I was going to take an even contrarian-er view but after typing it out three times I have to agree, it would be better to just separate them by arity and intent. I frequently use the index parameter, but I never do with a function reference because it’s a footgun from below. And mapping over entries is a well-established pattern, at least enough so that it’s worth the inconvenience of having to do it explicitly.


What is and what should be are separate discussions. But if we want to delve into the topic of what should be, I don’t think adding many different functions for all these cases where this can happen makes sense. It doesn’t scale. The problem, if one believes it’s a problem, is that javascript and typescript are fine with optional and implicit undefined arguments so they never require argument count to line up.

Just use an arrow function to wrap the callbackFn.


We’re talking about one additional function and I don’t think it’s reasonable to extrapolate it to many others. Which is why I started at your position but talked myself out of it trying to make the point.


I've found that people that learned class-based React first and then switched to modern function-based React often have more trouble with useEffect than people who learned functional components directly

I think a big part of the problem is we need to stop selling useEffect as a replacement for the lifecycle methods of class based components. It also probably would've been a lot less confusing if we called it something like useSideEffect

I think any react linting setup resolves most of the confusion but there's a lot of people that start off and don't even know how to set up lint rules for react. They should be a default in any react app imo


> I've found that people that learned class-based React first and then switched to modern function-based React often have more trouble with useEffect than people who learned functional components directly

I’d say its the opposite; people who used lifecycle methods know that useEffect and friends eliminates an entire class of bugs. People who started with hooks dont understand the problems it solved, and only see the quirks


This was not the only design possible. The main problem with lifecycle methods was that you couldn't have multiple of them. There is absolutely nothing about the design of React that necessitated hooks to fix that issue.

You could have this API

  this.addListener('mount', () => {
    // do things on mount
    // return cleanup to be called on unmount
    return () => cleanup()
  })
called from the constructur of a component. With this you could setup multiple listeners and make sure they're all cleaned up.

But no, the syntax wasn't "clean" (you'd pass `this` as an argument to the "custom hooks" equivalent"), so instead we got this error-prone order-dependent design that doesn't allow conditional execution and runs on every render.


I'm far more experienced with React function components than with class components, so I'm curious, what are some of the problems with class components and how did hooks solve them?


I can second this... I inherited a huge pile of crap without a react lint ruleset and as soon as I put it in place there were ton of errors.

The part I don't like about useEffect is that developers tend to overuse it and when they get stuck in infinite render loops you can see the whole mess it can lead to and how hard it can be to untangle monkey patched logic.


I think that particular choice was an unusually big mistake. Most of them are not so big. But I can't fathom why they made "empty array" cause no rerenders but "no array" cause all the rerenders.


useEffect is treating undefined as meaning undefined.

It makes perfect sense from inside of useEffect.

If there are no dependencies ([]), the dependencies can never change, so React can always reuse the old effect.

If the dependencies aren’t defined, there’s no way to tell if the old effect is OK, so React must always rebuild it.

**

But `useEffect(()=>{})` doesn’t explicitly show that React will get `dependencies = undefined`, and in this case that’s unusually important.

You could have ESlint force you to change that to `useEffect(()=>{}, undefined)`, I suppose…


Yes we know how it works. What we’re saying is that it is a cryptic design. Just like the function returned from useEffect is used for unmounting the component. Not intuitive at all.


there is an eslint rule exhaustive-deps which really helps with this


For useMemo and useCallback, Typescript and exhaustive-hooks will both tell you to add a dependency array, because without it those hooks do nothing.

For useEffect, exhaustive-hooks & Typescript say nothing, because a non-memoised Effect is a reasonable thing to write. It’s just not a great thing to write accidentally.


There's a few, rare, use-cases where you'd actually want this behavior. But I agree it's too easy to accidentally do this thing that should be an extreme edge case.


I've run into those cases a couple times before and my immediate thought was that it seems unwise for this one function to be used in both cases. There really ought to be something specifically intended for effects with no dependencies even if all it does is make the difference explicit. I'm not a fan of things that contain features that manifest like subtle traps because determining programmer intent is undecidable. That lesson was learned from PHP <=5.


This is why whenever I can (i.e. it’s not a breaking API change or it’s agreed on), I turn these implicit rules into explicit options named and documented as such in the code. Yeah it’s a little more to type, but it solves the “this isn’t obvious for good reasons” problem much better than a pair of defaults that you probably don’t even know how to look up.


At the very least make them separate functions


Yeah, that's what I'm saying. There's no reason they had to be the same thing.


I'm pretty sure because it's basically an easy hack to recreate the behavior of componentDidMount.


Dark pattern implies malice. IMO this is at worst poor forethought.


The original design was fine, the hooks redesign was a total miss


It's a design choice to import the functionality of componentDidMount into the hooks pattern




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

Search: