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.
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.
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.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.
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.
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.
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.
(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.
- 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?