Frustrations with React Hooks
blog.logrocket.com
blog.logrocket.com
And yes, hooks need to be grasped, they're quite something else. But once you get the hang of hooks, they're very simple to understand.
OP seems to be a bit stuck in a hole.
If you end up in a situation where your hooks come loose and the code becomes a mess — just delete them all and rethink your code / component logic structure. Believe me, it all can be simplified. Just rinse and repeat until you are satisfied with the outcome. That's when the "aha!" moment comes.
At the moment I only use `useEffect`, `useState`, `useRef` — the rest is unneeded so far even in my large-ish application.
P.S. sorry for the puns!
There are some weird edge cases with useEffect, no question.
I don’t think they’re bad enough to want to go back to using classes... but I think the OPs post was a bit more specific than you’re giving them credit for, and they stuff like useAsyncEffect exists because it is a common pain point.
For example, this useRef / useEffect combo mystifies me even now: https://stackblitz.com/edit/react-ts-zhvuha
The behaviour is weird sometimes; just because you can use the trivial use case easily doesn’t mean you won’t stumble, and if you do, good luck trying to understand what went wrong.
> ...they're very simple to understand...
They’re not simple.
They just superficially appear simple, which is good enough most of the time.
I think it’s entirely fair to complain that’s not ideal... even if you still think there’s a major benefit to the trade off compared to classes (which I do think is true too).
...but I’m very sympathetic to people hitting edge cases.
On this particular point, the reasoning is that refs are meant for values that don't need to trigger a rerender[1] -- an escape hatch, rather than something you reach for by default.
In that sandbox, you can accomplish what you mean to accomplish by using state rather than a ref.
[1] https://github.com/facebook/react/issues/14387#issuecomment-...
I'm not sure if there is a less confusing way of doing it, but it's damn confusing.
This is simply a matter of reading the docs (or making the mistake until you learn). Not an inherently more complex characteristic than, say, Class component lifecycle methods, which also required understanding their rules and reading the docs.
Once you know refs updates won't re-render, you won't make this mistake.
I don't understand why "a thing you need to learn then you're good" is somehow inherently more complicated than ... other apis that you need to also learn.
> but most people think of refs as...
This sounds like Dan Abramov's comment that some of the struggles with hooks is from people who have experience with react without hooks, vs people coming new to the whole thing. So maybe it's about relaxing pre-conceived notions until experience takes over.
Also that the fix is "instead of using this one kind ref, use another kind of ref and put it in state"... I don't know, like I said I'm not sure if there is a better solution, but it still feels kind of unintuitive and complicated.
Now that I'm thinking about it... what is the reason that DOM refs and other refs need to be handled by the same concept? Every time I make a DOM ref, I'm doing something with it in a hook like useEffect. Why make me jump through hoops to re-run the hook if the DOM ref changes?
(I recognize there are probably good answers to those questions, the React folks are great, I just don't know the answers! Is it just to avoid introducing one more "type of thing", and instead making refs and DOM refs the same "thing"?)
Ref's are basically a 1 to 1 replacement for instance fields for mutable data (that doesn't cause rerenders).
https://reactjs.org/docs/hooks-reference.html#useref
There's always need for escape hatches and maybe I'm missing your use case, but in the context of a discussion about how hooks are more complicated than classes, what were you doing before with refs to solve your "re-render" on change scenario?
Your example was likely contrived, but modifying innerHTML should be replaced by putting whatever in state and simply rendering it. And use state for dependencies where you want to re-run on change. Refs are just another way to keep state but not have it affect render cycle.
If you're not aware of that, it is very tempting to use `useRef`, which is what I have often seen. Before hooks, we did not have the temptingly-named footgun `useRef` for this scenario (although we did have other footguns for other scenarios, and overall I love hooks).
It’s harder to just use state for example, when you need a ref to a canvas to get the 2d context.
The classic X here, of course, is monads.
But as personal examples, it took me ages of headbanging to get the 'aha' moment for git, even though a bunch of people I know got it almost immediately.
In the case of hooks, once I built a mental model of how they were (theoretically) implemented, I went 'ooh' and found them obvious and relatively easy from then on. It looks to me like the author of this piece didn't have that luck, and so for him, they're not simple (although maybe later, post-aha-moment, they will be).
tl;dr: "They seem simple to understand once you understand them, but are mystifying up until that point" seems to me to be the most likely hypothesis here.
One thing we haven't been able to replace, though, are classes that depend on refs. How to do this, since there's no instance with functions like there are with classes?
If you ran into something that isn't quite covered by this hook, I'm also curious! Can you write a minimal example in codesandbox and share?
https://reactjs.org/docs/hooks-reference.html#useimperativeh...
* Removed "pure" to make my main point clear.
But my point was functions are simpler in all those scenarios, pure or not. Edited my comment to reflect that.
Sure, if you define a function called useWhatever that doesn't actually call any hooks, that might be a pure function. But the restrictions described in https://reactjs.org/docs/hooks-rules.html are not restrictions that pure functions have.
It more-or-less boils down to: 1. class-based components force your lifecycle logic to live in disparate locations. 2. Class-based hooks are obtuse with hidden gotchas whereas pure functions tell you exactly what they're doing. 3. A whole class of unergenomic solutions disappears when you use hooks.
Hooks have their share of weird issues; they explode if they're called in a different order/number from how they were called the first time the component rendered (making them not at all pure functions), and it's super easy to screw up the dependency array to useMemo/useCallback (so either your hook is useless and re-installs the event handler on every render, or you get stupid "the callback had a stale copy of the state" bugs not possible with class-components).
I've also seen my fair share of situations where what would have been a this.setState with a callback becomes a tangled mess of useStates and useEffects, or someone bends over backwards to try and avoid re-installing an event handler every time one of the props or state variables it uses changes.
I definitely get that there are some more complex cases where you have to jump through a couple of useRef hoops to do what you need. On balance though, I think the way the behaviour becomes declarative is worth the trade off.
And you already have to wonder if a function has state if you didn't write it. If you did, you should know.
You have more control too. Look at the parameters to useState to see what I mean.
The class API lifecycle methods are kind of a mess, and useEffect() seems like a more convenient abstraction. But I could imagine replacing the lifecycle methods with hooks that are added at object construction and get similar benefits. I wonder if the problem is not so much classes, but just the API that happened to evolve.
“Just burn it to the ground and start over,” isn’t an inspiring endorsement of the methodology...
The only real dislike I have, is in how `useEffect` confuses the React lifecycle. It took me a little while to grok the lifecycle idea, but it was/is a useful way to conceptualise what happens to your components at runtime.
But now, you have to remap all that knowledge to the hooks workflow. It's not a huge challenge, but it's yet another overhead.
Indeed, starting out, our team ran into a number of issues with infinite rendering loops and the like. Took a fair amount of reading to discover where the pitfalls were.
See the following resources for more details:
- Mark Erikson - Reactathon 2019: The State of Redux (https://blog.isquaredsoftware.com/2019/03/presentation-state...)
- Mark Erikson: Redux - Not Dead Yet! (https://blog.isquaredsoftware.com/2018/03/redux-not-dead-yet...)
- Dave Ceddia: React Context API vs Redux (https://daveceddia.com/context-api-vs-redux/)
- Mike Green: You Might Not Need Redux (But You Can’t Replace It With Hooks) (https://www.simplethread.com/cant-replace-redux-with-hooks/)
- Sergey Ryzhov: From Redux to Hooks: A Case Study (https://staleclosures.dev/from-redux-to-hooks-case-study/)
- Eric Elliott: Do React Hooks Replace Redux? (https://medium.com/javascript-scene/do-react-hooks-replace-r...)
- Chris Achard: Can You Replace Redux with React Hooks?](https://dev.to/chrisachard/can-you-replace-redux-with-react-...)
I usually feel like I'm writing too many effects which update state(s) and the lifecycle flow becomes harder to contain mentally, however there are times where I'm like "this is definitely easier or less of a cluster then it use to be".
So I think the jury is still out. In terms of Hook's initial premise of "we're doing this so that developers unfamiliar with class-based OO paradigms can work better/faster", I don't think it added any more clarity or ease-of-use over the class based lifecycle methods tbh.
I'm genuinely curious if the difference is because I was doing tons of Recompose/HOC style components before hooks came out.
Also, just FYI, it didn't click for me until this tweet[1]:
> The question is not "when does this effect run" the question is "with which state does this effect synchronize with"
> useEffect(fn) // all state
> useEffect(fn, []) // no state
> useEffect(fn, [these, states])
[1] https://mobile.twitter.com/ryanflorence/status/1125041041063...
It'll only become apparent when you app is large and complex enough that it suddenly starts to matter when things re-render and then you'll be faced with tracking down those re-renders and fixing dependency arrays everywhere.
With classes, the naive solution is the right one when it comes to preserving function identities across renders.
Not saying hooks are bad, but they're not all roses and sushine.
When a component is rendered, React will recursively re-render all descendants of that component. Out of the box, React doesn't do any optimizations like "skip rendering this component if the props haven't changed". Because of that, "re-creating callbacks" isn't an issue in base behavior [0], because there's nothing that cares if it's a new function reference or not.
It's only when child components are attempting to optimize perf by comparing props and avoiding re-renders that having consistent callback function references matters (ie, `React.memo()`, `PureComponent`, and React-Redux's `connect()`).
[0] https://reactjs.org/docs/hooks-faq.html#are-hooks-slow-becau...
> Internally, React uses several clever techniques to minimize the number of costly DOM operations required to update the UI. For many applications, using React will lead to a fast user interface without doing much work to specifically optimize for performance.
My experience with React applications (both writing them and as an end-user) hasn't really borne this out.
I hoisted the state of an input box up a few layers because other parts of my app are affected by the current typed content. Since I'd been careless passing closures in as props to expensive components, they were getting re-rendered on every keystroke. It was noticeably sluggish.
That's the case I was talking about, I probably should have added that. In our codebase at work most components are using such optimizations but of course that's not the case for most people.
I just profiled it on my 5 year old MacbookPro, and out of 50 commits or so, just a single one spiked above 20ms (which was the first commit, subsequent to same view were much faster) and almost all are in the <5ms range. I decided it is not worth doing any optimization yet, and I will defer useCallback/useMemo optimization until there is a real, noticeable delay in rendering.
Amen to that. The fact that still are in "here's a 'better' idea, let's try this" landscape in JavaScript is depressing.
I no longer jump on these new frameworks when the bandwagon flies by. I ignore postings for jobs saying they are rewriting their system in "new Framework Y!". I don't care how "pure" your new design is, I care about having it work well in the trenches.
I, too, am tired.
And old.
I guess I'm old too. When someone shows me what (real) pain hooks solve on non-Facebook-size codebases, I'll be all ears. Until then... Thank you, classes work just fine and make my code nice and readable.
But hooks creates abstractions the developer needs to deal with that never existed before.
Hooks are not functional because they break referential transparency of functions.
You have to track dependencies manually and hooks are more difficult than they need to be for the "componentDidMount" equivalent. If you don't get the dependencies just right you end up with things not firing or in infinite loops.
You have to wrap your functions in "useCallback" or "useRef" just so the reference doesn't change and cause infinite loops.
You can't create abstractions where a hook calls another hook. So you end up having to inline a bunch more code into your function rather than outsourcing it into a helper function.
The positional order seems like it would be easy to work around if they allowed you to pass in a key. Not sure why that isn't available.
Not sure if I understand you correctly, but isn't this the purpose of custom hooks? You should be able to freely call hooks within other hooks.
Of course you need to follow the same rules in the hook (always call every hook it calls, in the same order, so you don't mess up the order above).
Its curious that I haven't found that to be a pain point.
IIRC it's because they implemented their own method lookup table (!) to associate with the Component object (!) but as a FIFO queue, more or less. I assume either for ideological (that's how they wanted it to work) reasons or because they (probably correctly) reasoned that loading down React apps with more strings at such a basic level would risk performance/memory problems. Plus if they did that then it'd really look like a method lookup table and be more obviously Rube-Goldbergian than it already is.
Custom hooks naturally fall out of the current implementation. Technically, React doesn't even know that custom hooks exist - it just knows that more of the primitive hooks are being called inside your own component.
If you've got time, skimming the original React Hooks RFC https://github.com/reactjs/rfcs/pull/68 ) is informative (and admittedly difficult, because there's hundreds of similar comments).
But React has deviated so far from both the OOP, FP, and traditional programming paradigms that now it kind of feels like hacks are needed to compensate for hacks.
It can easily be solved by just setting the state in the event handler, since the state is included in the dependencies array for the fetch effect:
https://codesandbox.io/s/dependency-array-0u3sc
The answer to this question posed by the author?
> On line 23, the useFetch Hook will be called once on the first render. On lines 35 – 38, pagination buttons are rendered but how would we call the useFetch Hook from the event handlers of these buttons?
We can call `useFetch` again by triggering a re-render. This hasn't changed at all with the introduction of hooks: new props or new state still triggers a re-render.
Though I'll admit getting the relationship of state and effects caused by changes to that state can be more difficult to work out mentally, I think it results in a more complete understanding of what a component actually does.
A good mental model is that the second argument to useEffect defines which pieces of state that effect syncs with.
useEffect(() => {}, undefined) // Sync with all state
useEffect(() => {}, []) // Sync with no state
useEffect(() => {}, [a, b]) // Sync with `a` & `b`
Recently I built a bolierplate JSON-api web app project in vuejs and golang (previously I was using TypeScript, Apollo GraphQL and Express). I find the terminology around vuejs much more user friendly and it comes with a router and state management built in...I try to avoid using any other JS module. The golang ecosystem is much more stable than nodejs which is a relief....I have a feeling I might just settle on vuejs + golang for all future web apps
The value returned from useState is just a plain ol’ variable like any other so it also gets captured by closures like any another variable. We had an editor that updated some state and then an event handler (defined as a closure) that did something with the state variable. The state was updated as expected but when the event fired, the closure had the original value. It can be worked around via a container/useRef (and is maybe a code smell in the first place) but it made for some painful debugging.
Thunks are sufficient for most use cases, but the main thing they can't do is respond to dispatched actions.
For more info, see the Redux FAQ entry on choosing an async middleware:
https://redux.js.org/faq/actions#what-async-middleware-shoul...
Not to mention the docs for useEffect just mention the one usecase of running on mount and on update and neglect to even talk about running something just on mount.
That being said, the reducer hook is amazing and a great replacement for overly complicated setState or overly simple Redux stores. Definitely reminds me of ReasonReact in a good way.
"It uses prototypes, not classes!" simply makes it a different kind of object-oriented.
- useState with an array is bad news if more than 1 component is consuming or setting the state.The clunky alternative is to keep it in a ref & call a forceUpdate whenever you would normally call your setter
- useCallback doesn't scratch the itch for things like document event listeners since everything inside the callback will be stale. The clunky alternative is a custom useEventCallback that keeps your callback function in a ref. (and that might not work in the upcoming sync mode)
- linter rules can be too strict & the --fix flag can actually cause a break in your app by adding things to the dependency list. Sometimes a useEffect depends on the current value of a ref, but linter says that's a no-no. 2 useCallbacks can be co-dependent, but there's no way to write that co-dependence in the dependency list. Sometimes I want to return null before a hook. The clunky alternative for all these is a bunch of eslint-ignore comments.
It sounds like you might be setting state by modifying the array and then calling the setter. This won't work:
array.push(thing)
setArray(array)
Instead, you have to update the array immutably, like setArray([...array, thing]).> useCallback for event listeners
useEffect is usually the right place to set up event listeners (and has a built-in way of cleaning them up, by returning a cleanup function).
> Sometimes I want to return null before a hook.
Hooks pretty much have to be at the very top of the component, and eslint-ignore'ing those errors will probably cause weird issues later. Better to think about another way to solve the problem that doesn't involve returning early.
An issue I ran into recently: I had a modal Edit dialog with some form state that was initialized with the current values of the thing I wanted to edit. If that modal was always mounted and merely shown/hidden (<Modal isOpen={itemToEdit}/>), the state would be initialized once and wouldn't update when I changed the item-to-be-edited. The fix was to unmount the dialog, and only mount it when there was an item to edit, {itemToEdit && <Modal isOpen={true}/>}
You're component gets rerendered when the array changes and not when you got the right mix of lifecycle methods going.
But yes, it's not always obvious what changes you really care for.
To me this is a feature rather than a shortcomming: I use immutable data structures all the time (with array and object spread operators it is really easy to do so) and the effect will only update when it really needs to - thus only triggering a re-render (or rather reconciliation) when something has actually changed. This is in contrast to .setState() which triggered reconciliation regardless if a value actually changed.
Seems very convoluted to do that inside the useEffect, just to show how "it doesn't work".
Especially because often a re-run of the effect will be desirable when some of the props change, not always the whole bag..
They didn't click for me until I read this netlify blog post: https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-rea...
If you think of them as mostly just syntactic sugar for closure state variables wrapped up in the module pattern, it's not so bad. (This is how some of us have been writing D3.js code for years!)
The market is ripe for a React like framework that keeps JSX and first class components but goes away with hooks in favour of a couple of event listeners and stuff like this.getContext(ContextName). Maybe also add MobX to easily solve state management. Vue is pretty close except JSX / first class components are an afterthought.
Edit: If you want to see how hooks are implemented without needing to understand the React codebase, Kyle Simpson has a project that provides hooks for non-React functions, the implementation is all in one file: https://github.com/getify/TNG-Hooks/blob/master/src/tng-hook...
That disclaimer aside, my biggest gripe is that I think hooks _could_ have been done in a "works from both OO and FP components" manner, so that all of these new (legitimately) "amazing to use b/c they're not HOC" APIs like the apollo hooks/etc. could be used by both types of components.
Instead, because hooks are they only non-terrible way of using libraries (again say apollo), users/codebases are basically forced to convert their OO components over to FP.
Specifically, to me the primary innovation of hooks is not "reinventing OO back into FP", it's "the ability for reusable code to attach itself to the component lifecycle".
I.e. a hook can know when componentDidMount/componentDidUpdate/etc. happened, and do the right thing. That's the primary innovation of hooks, IMO.
That would be extremely useful for OO components too, and also very doable, like just expose a component.addComponentDidMountCallback(...) type methods (or component.addEffect(...) or frankly even useEffect could do this b/c React implicitly know which component is being invoked right now).
With something like this, I think all of the "currently-FP-only" hook libraries could have "OO-based" equivalents, that are just as pleasant to use, and users could choose which paradigm they preferred on their own.
BUT, the biggest annoyance is that libraries would have to maintain both "FP hook" and "OO hook" versions, b/c current/FP-only hooks API didn't consider OO components a constituent in the design process.
(Specifically the only deal-breaker/breaking-API-change to use FP hooks from OO (if React wanted to allow this) AFAICT is that useState returns a tuple of [value, setter] instead of a State interface with getter/setter methods. If it returned a State getter/setter, then the useState could be invoked outside of the OO render() method, but then inside of render state.get() would return the current value. AFAICT all of the other hook APIs are already "OO-compatible".)
It just seemed like it was thrown in there because Java and C# developers who were increasingly being forced to use JavaScript begrudgingly just couldn't live without their "class" keywords, so they took to the forums to make JavaScript more like a "real" language.
Plenty of the people who dislike Javascript's prototypal inheritance model and (relatedly) scoping rules do understand them.
I know what you mean, but in practice it’s easy to retrofit a hook into a legacy class component by wrapping the hook in a tiny functional component. Sure, you have to find some other mechanism (e.g. make it a HOC, or use render props, etc) to make that wrapper component compose nicely with your class component, but that’s the price of not porting the whole thing over to hooks.
I have run into a sort of useEffect hell at times that makes me wish for the old life-cycle components as visually I find them easier to comprehend in very "active" and more complex components. But honestly I suspect that is because I don't quite understand useeffect well enough.
They're not perfect. To each their own opinion. But they are what ES6 did for me to the language landscape.
I just wrote a tutorial that shows how to use Redux Starter Kit, TypeScript, thunks, and React-Redux hooks together. Should hopefully be a big help, for the reasons you described:
https://redux-starter-kit.js.org/tutorials/advanced-tutorial
Yes, Hooks take a bit of getting used to and until you get them they will be ... weird. I did a few mistakes trying hooks until I actually "got" them (the fact that I tried to use hooks without actually understanding them certainly didn't help). But learning to do them right was certainly not harder than learning the quirks of classes.
In the beginning I thought they complicate the code unnecessarily.
Until I realized they can just be extracted into custom hooks.
The real power of hooks, besides the fact they are declarative!, is composition and by means of composition they can be abstracted away as custom hooks.
React is all about declarative UI composition but lacked the “primitive” for having stateful logic composition in an elegant and declarative way.
Until hooks!
`const { data, loading } = useFetch("/query", {page});`
is not only shorter than the corresponding slew of lifecycle method implementations that would be required in a class component, it's also much more direct and readable. Trying to recover the intent of code by reading the implementation of various imperative methods is painful and error-prone, whereas it's hard to imagine how the above could any more effectively communicate the intent - every single token is meaningful.
I remember the pain building an app with redux and constant mapToState.... that was nightmare! With hooks it's so easy now.
The React-Redux hooks API [1] also requires less code that `connect`.
I just wrote a tutorial for RSK that shows how to use it with TypeScript, thunks for async, and React-Redux hooks [2].
[0] https://redux-starter-kit.js.org/
[1] https://react-redux.js.org/api/hooks
[2] https://redux-starter-kit.js.org/tutorials/advanced-tutorial
Those react-redux hooks just exemplify the issues I have with hooks.
Yeah, potentially less code, but with more (and in some instances, completely unheard of) gotchas, more that the developer has to do to keep parity with not using hooks, and certain things dropped entirely.
I'm super interested in this discussion. I've been using React for years, and it's been smooth sailing. But somethings up with hooks, and I haven't been able to put it into words.
- The long-standing problem of trying to synchronize an external synchronous state container with React's async rendering cycle
- That our existing solution for this problem requires overriding values in context for connected components
- The fact that context usage _require_ rendering a <Context.Provider> to update a value
- The fact that hooks themselves do not do any rendering
So, the solution we have for avoiding stale props only works with `connect`. It's not that the hooks themselves are problematic or have inherent gotchas, it's just that hooks don't offer the specific additional capability we would need to implement that same solution in both places.
A user just put together a _fantastic_ article that dives deeper into this specific problem, and recaps how each version of React-Redux tried to solve it [1]. Highly recommended reading if you have some time.
[0] https://react-redux.js.org/api/hooks#stale-props-and-zombie-...
[1] https://kaihao.dev/posts/Stale-props-and-zombie-children-in-...
I've encountered the issue before, but never knew the name. After bumping into it a couple of times I just changed up how I use react-redux.
At first, React hooks look simple and easy. But when i started thinking about setTimeout's, API calls, Re-renders, the flow get's difficult to imagine.
The big wins for me were:
- passing an empty array as the last argument of useEffect makes useEffect work in a similar fashion to the old ComponentDidMount. You can have any number of useEffect invocations in a component, and for a component that is, say, loading data from two different sources, I find having two discrete useEffects loading just what they need much more clean/intentional than putting everything into a single lifecycle method.
- using Hooks with Context almost completely fills the giant state management hole that has been (imo) hampering React since its inception. Hooks + Context is a cleaner and more comprehensible solution than React + Redux for any application that has a reasonable amount of state complexity. For data-heavy applications, Redux may still be a good choice, but for everything else, the Hooks + Context combination is hard to beat.
In a single view (for lack of a better term), I'd guess the largest number of components that access either context is six.
- 'render props' are fairly awkward to write, and add a lot of noise to a component that's working with data from multiple contexts.
- this.contextType can only give a component access to one context. This is a pretty big problem.
With Hooks, useContext lets you reference and destructure contexts in a really elegant way. Say you want to show a user's name if they're logged in, with the render prop approach it's something like:
<AuthContext.Consumer>
{({ isLoggedIn } => {
return (<div> {isLoggedIn && <span>i'm logged in</span>} </div>)
})}
</AuthContext.Consumer>
with useContext, you can do const { isLoggedIn } = useContext(AuthContext);
and then use {isLoggedIn && ...}
to condtionally render anywhere in the body of your component without all the Context.Consumer business. class MyComponent extends Component {
static contextTypes = {
auth: AuthContext,
}
}I'm not sure this a valid understanding, but it works for me as a heuristic.
I totally can relate to the frustration when learning new stuff, especially when you have done things one way for years. Personally, the first month or so that I used hooks in react, it wasn’t pleasant, mostly due to me being uncomfortable with the additional load of learning this new convention.
Suck it up buttercup ;)
I didn't feel that want when picking up React originally, but I do with Hooks. That's a giant red flag in my opinion.