Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Alright so, reading through all this I think I can summarize it:

- Hooks are tricky because you need to pass them an array of dependencies, which is manual housekeeping

- You shouldn't pass anything but primitives to a hook's dependency array if at all possible

What is the alternative? Just pay attention to the two above, or go back to class based components? Or will there be a React-flavored JS/TS (like JSX / TSX) that has different closure mechanics?



> ...or go back to class based components?

Was there anything wrong with class components? It's what I learned half a decade ago, and the idea of a "state" object made so much sense. Now, with hooks and whatnot, it seems like React is trying to be "functional" without actually being so.


I’ll prefix by saying I agree with a lot of the criticisms, and I’ve experienced first hand how hard to explain and error prone hooks are. But for this post, let me defend the concept.

The idea, in a very rough nutshell, is to allow separating behavior and presentation.

Hooks are the reusable unit of behavior. You compose hooks into more complex hooks that might implement loading and saving data from/to the server, for example.

Then you can use this hook with different components, or use the same component in a different context with a different data source. This can be very powerful if used well.

But as I said at the beginning, hooks are unfortunately also very difficult to get right.


To expand on this, many of the things that hooks now make easy were bespoke per-class implementations with details that leaked into every lifecycle hook. Think of how you’d write a chain of useMemo calls in a class component to see just how bad it was.


Why would you need useMemo in a class component? You can just attach a memoized instance of the function to the component


State wasn't evil, the lifecycle callbacks were. I don't think they ever deprecated setState, but I'm pretty sure they've nudged people away from the lifecycles..

Class components when needing more complex behaviours related to lifecycles/context had the choice of:

a. Hard wiring up all the different lifecycle events to bespoke systems, on a per component basis (use same thing in 5 places? Implement it 5 times)

b. export const ActualWorkingComponent = HOC(HOC(HOC(HOC(NonWorkingWithoutHocComponent,{conf4}),{conf1}),{conf2}),{conf3});

I will fully admit that useState is more aimed at singular/simple values, so a more complex object is a pain to directly copy over. (useReducer or wrapping setState could work, as the child components shouldn't re-render unless their props change).

But it is so much nicer to have the dependencies at the start of the the functional component, and not in a horrid callstack at the bottom, with 3 different ways of defining which prop gets which HOC's values/functions and then injecting even more callbacks to munge those values from the out HOCs to use in that HOC to make it's output good for next HOC...

Instead, it's just:

    const {widgetId} = useRouterParams();
    const widgetData = useSelector(selectWidgetById(widgetId));
No mess, no fuss, use it it in a few places? Wrap it up in a function of it's own.


With class components, lifecycle management is trickier to encapsulate and reuse since you have to split such things up and scatter them across several different methods or wrap the whole component in a HOC, and in general class components require more boilerplate. Thus you'll usually end up with code that's just a bit more spaghetty than with function components. With hooks, you can at least encapsulate some specific behavior behind exactly one function call without jumping through any extra hoops.


I also liked/like the idea of one state object. With hooks now they really discourage packing everything into one state object and you either have to use a bunch of different `useState` statements for your different variables or pack a big state object in one `useState` but then you face big performance issues with needless re-renders when you only change one element of the state object.


Take a look at useReducer(no 3rd package, part of the React api)


I would say mixing state and behaviour was the main problem with class components (and oop), although it might be a better problem to have than all the approaches supported simultaneously (some code with class components, some hook heavy, etc)


Jetpack Compose solved this problem in two ways:

1. State you read in a ~hook~ aka Composable is/should be an instance of a special kind of observable object. You can create your own subclasses or just use the standard state container much like useState. This means the system has more information to produce minimum subscriptions/reactions at runtime.

2. The system used crazy compiler transforms to turn functions marked @Composable into reactive scoped hooks/components. Using the compiler eliminates a lot of error-prone boilerplate and bookkeeping code otherwise required for these kinds of systems in standard OO languages without monad+do-notation by adding a sublanguage “manually”.

Downside to the Compose model is that it’s even more mindbending to understand. Developers are encouraged to surrender to the magic. I’ve yet to read/write enough Compose code to understand the cost benefit analysis yet.


Is SwiftUI pretty much the same in this regard? I’ve only taken time to do the trivial examples in either Compose or SwiftUI, but they feel very similar, so I’m wondering if your prognosis also applies to SwiftUI.


I think the Swift compiler does less magic for SwiftUI. They added a new literal syntax for the UI view tree literals, but other than that I believe the behavior is in the runtime. SwiftUI expects you to use Combine, Apple’s (F)RP system, to a greater extent than Compose expects you to use RxJava/Kotlin Flow. My impression is that Compose is more React-y and is actually an escape from RxJava FRP-land; I can translate most hooks to Compose after reading the Compose docs a few times but find it much harder to do the same with SwiftUI/Combine.

Personally I prefer Compose because it’s open-source (so you can figure out how it works if you need to), has much better docs, and seems less welded to FRP stuff which I don’t enjoy. I don’t really have enough experience to really review either though.


> What is the alternative? Just pay attention to the two above, or go back to class based components?

React may have limited options with this design, but other frameworks have taken other approaches to the problem:

Vue/Svelte/MobX only run the setup code for hooks (or closest equivalent) once. Derived values and effects are automatically run without specifying dependencies - the tools detect what an effect reads while it runs, and track dependencies for you. Since effects are only set up once, closure values from the setup scope don't expire/disappear, so they can't go stale in the same way as in React (caveat destructuring). I think Solid is in this camp too, but I haven't used it.

Frameworks like Mithril and Forgo ditch state tracking and effects entirely. You explicitly tell the framework when to rerender etc., and everything else is just you calling functions and assigning variables without the framework's supervision.

Crank.js extends the explicit-rerender idea by using generators for components. This preserves the "a component is just a function" feature from React, but avoids the hooks pitfalls by only executing a function once.

Hyperapp doesn't have the notion of components at all, so you can't have component-local state. The framework reruns all view code at once, passing the current global state. You can approximate components by writing functions that slice and dice state and divide up the work, but that's transparent to the framework, and there's no place to store state besides the global state.

These all have trade-offs. They may require more complex runtimes / toolchains, or simply shift around the burden on the programmer (what's easy/hard, what kind of bugs will be common).

I'd love to see more approaches in this space. Not all trade-offs are right for all situations, and I'd like to see more ideas that meaningfully change the development experience, rather than "if you squint it's basically the same thing" ideas.


> I think Solid is in this camp too, but I haven't used it.

Correct. Solid is all about signals (reactive values). When you run any effect (rendering updates are effects created behind the scenes for you), it will get run once immediately, tracking which signals where called. Then it will subscribe to those signals to re-run the effect on change, and it will resubscribe to newly called signals, and unsubscribe from no-longer called signals.

I believe that it is roughly equivalent to Vue's reactive api, except that rather than using a proxy or setters to allow object mutation to trigger effects or re-render, it uses separate update functions, more like react hooks do.


It’s often not even that much manual housekeeping. If you follow the second advice, then ESLint will do a fine job of telling you exactly what’s missing or superfluous.

(I argue that ESLint is almost required when working with React. You can turn off its weirder other rules if they become an annoyance, but the hook rules are golden.)

No, I think the problem is the combination of closures, mutability and identity. Very few other things in programming punish you that harshly (and subtly) if you don’t have a crystal clear understanding of all three concepts.


ESLint is fantastic, but what would be more golden is unit tests failing or code not compiling. It is almost like hooks need to be written in a meta language like JSX so that they can be compiled and thus failed when written poorly.


I think most specifically the call out in the article is: the Hooks that React provides are extremely "low-level" (and intentionally so) and while you can do everything with just raw low-level hooks, consider returning to higher level hooks. The article provides some simple (Typescript type-safe examples) of higher level hooks. It also recommends that Redux/MobX/Relay/etc are still very useful higher level tools, even or especially in hook-based components in React.


Unless I am missing something, functional components and centralized state (a la elm https://guide.elm-lang.org/architecture/) are enough. Bringing global state (Context called from anywhere) can make it less clear.


A sane solution for exactly this problem has existed for a decade now: https://www.qt.io/product/qt6/qml-book/ch04-qmlstart-qml-syn...




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

Search: