Hacker News new | past | comments | ask | show | jobs | submit login
A Critique of React Hooks Addendum (dillonshook.com)
73 points by vicarrion on May 7, 2020 | hide | past | favorite | 50 comments



Maybe a good place to ask this:

I've been hearing a lot of "oh we don't use Redux, we use hooks" lately, as if this obviously makes sense.

Am I missing something? To me this seems like "oh we don't use Redux, we use arrays". I'm gonna need quite a few more details before I can make any sense of a statement like that.

Like... what? How... how does that explain what you're doing? One of these things is not like the other. "Oh we don't use doors on our buildings anymore, we've switched entirely over to trees". Huh? What the actual hell does that mean?


Hi, I"m a Redux maintainer.

Context and hooks don't "replace Redux", but they do overlap with some of the scenarios and use cases that previously led people to choose Redux.

Please see my post "Redux - Not Dead Yet!" [0] for information on how Redux compares to tools like React context + hooks and GraphQL, as well as situations in which it makes sense to use Redux.

I'd also suggest reading my post "React, Redux, and Context Behavior" [1] and watching my Reactathon 2019 talk on "The State of Redux" [2] for additional information on these topics.

As you noted, "hooks" and "Redux" are not exclusive. We released our React-Redux hooks API [3] last year, and it's been pretty enthusiastically adopted. We're now updating our docs to show using the hooks API as the default instead of `connect`.

[0] https://blog.isquaredsoftware.com/2018/03/redux-not-dead-yet...

[1] https://blog.isquaredsoftware.com/2020/01/blogged-answers-re...

[2] https://blog.isquaredsoftware.com/2019/03/presentation-state...

[3] https://react-redux.js.org/api/hooks


One of the problems Redux is used to solve (as a global store) is peace of mind that you won't need to refactor large swathes of component trees to reparent state and callbacks that need to be shared or persisted across different subtrees as new business requirements arise. Hooks (especially custom hooks, which are just a composition of other hooks packaged together as a single function) make the reparenting/hoisting/anchoring of state and callbacks trivial compared to other mechanisms, providing similar peace of mind that you're not boxing yourself into an inextensible component tree. (This is regardless of context; passing down props isn't a large pain point, and is often misguided to try to solve because it leads to importable components and hidden contracts.)

Render props and HoCs solve similar problems for composition/reuse of functionality (these are all mixins at heart), but the hoistability of hooks is really the distinguishing mark in my experience.


Replacing redux with hooks that return local state can be a road to hell, as calling the hook somewhere down the tree will duplicate the state and there is no good way to prevent developers doing that.


IMO that's not much different than introducing a new variable in your global state to represent the same thing because you didn't realize there was already one there.

If you have a team you need discipline, code reviews, and leadership. No state solution is going to solve this for you.


It is not a matter of discipline. Avoiding this gotcha requires you to manually inspect your entire parent component hierarchy looking for uses of that hook - sometimes that is a plain grep away, but it is a very much invisible error. You also have to be familiar with the inner workings of the hook itself, and any other nested hooks, to verify if they use any local state or not. If it does, and you need to share that state, well, that's a big rewrite which kind of defeats the initial argument.

Maybe extra tooling could be built to avoid this, if we weren't already drowning in linter plugins...


Again this isn't a problem with hooks. You could easily connect a redux store and reducer to some component low in the hierarchy with it's own state. Or use a class based component with some state. A developer can introduce random state anywhere and it has nothing to do with hooks themselves. If you can't trust your team to make the right decisions about where to place state, then you need to provide more guidance.

I also disagree about the big re-write. Converting a hook's data/state to come from a prop instead is a very simple change.


I love how every criticism of hooks is like "if you don't take care to use them well, you get bad code".


I always try to tell folks that frameworks won't save them from themselves.


It sounds like you're talking about network resolved state. I like to share this sort of state using a subscription model, where components subscribe to a property of a state root (which anchors/owns the state and how to fetch it), resolving that property's state when mounted. Where that root is in the tree determines the lifecycle of the state (e.g. it's gone when unmounted), and multiple subscribers to the same property can mount, unmount, etc. at any point during that lifecycle without duplication.

I believe this model has similarities to both Apollo and Angular services, though I don't have direct experience with either.


No, plain useState. What you're describing can be done via Context, which gets you back to "need to refactor large swathes of component trees to reparent state and callbacks that need to be shared or persisted across different subtrees as new business requirements arise", but worse.

EDIT: quick example here https://codesandbox.io/s/unruffled-easley-ry6x2?file=/src/Ap...

This is a fairly innocuous example since the bug is immediately apparent from the button not working. Now imagine the failure mode is more subtle, and these components are about two dozen layers apart down the tree.


That's not a bug with hooks, but almost a complete misunderstanding of how hooks work (*on the part of anyone who writes that code thinking it will behave otherwise). Hooks are effectively instantiated on a specific component instance. Calling useState multiple times like this is multiple instances of useState, whether or not it's wrapped in another function.

And I'd like to be very clear that I strongly advise against cutting through layers with context because I wholeheartedly agree with your assessment there. You can plumb down those handles explicitly through props and enforce they're provided in a type system.


Of course it's not a bug, that is not in question. As I mentioned above, the problem is that even if you are fully aware of this, you still have to check that whatever `useSomeStatefulThing` hook you used to replace a global store does not use local state. So the assumption you can simply replace a global store with localized hooks to manage state is not true, and will certainly lead to bugs. You must use Context instead (or proo drilling as you suggest).


Since you provided a concrete scenario, I should probably do the same to clarify what I'm referring to in re-parenting or hoisting hooks. It doesn't really map to the statement "replace a global store with localized hooks", so I'm probably doing a poor job communicating. Similarly, I do not assume "you can simply replace a global store with localized hooks", because like you say, that's not true, and I can appreciate you clearly understand how hooks work.

At the same time, unless I'm really misunderstanding the example you shared, I can't see the problem or mental overhead you're mentioning where we need to be vigilant about having the same hook used in multiple places or understanding the scope of the hook's "local" state and callbacks. That's the point of hooks as a unit of encapsulation -- just like with a class, each use of a hook is a different instance, and the scope is that of the instance (and hook instances are scoped/bound to the lifecycle of the containing component instance). The code in the problem you showed is equivalent to calling setState on the one component instance and expecting that to show up on another instance which happens to be of the same component class. If we understand how React component instances work, that's clearly not the case (state isn't broadcast across instances), and the same goes for hooks.

Looking at what I mean by the improved portabiliy/hoistability of hooks I mentioned in previous comments, let's say we have three components: App, Foo, and Bar. App renders Foo and Bar as children.

We have a business requirement that Foo has some behavior which contains multiple pieces of state and bindings to multiple React lifecycle events (e.g. mount). What we don't know is whether we'll ever need to share that behavior with Bar.

React class API: We need to write bindings for this behavior against class state and lifecycle methods, intermingled with other code. This intermingling means you know there's a good deal of refactoring to do to move the behavior upward to App if we ever need to share the behavior with Bar. As a component grows, more and more logic intermingles on those lifecycle methods and makes refactoring more challenging.

  class Foo extends React.Component {
    state: {
      behaviorState: {...},
      unrelatedState: {...},
    };

    componentDidMount() {
      initBehavior(this.props.input);
      initUnrelated();
    }

    ...
  }

React hook API: We can easily write this behavior in a custom hook, encapsulating the behavior in some combination of useState, useEffect, and other React hooks. Now we have a self-contained useBehavior hook, by definition isolated from any local state, which you can choose to use within Foo.

  const Foo = ({ input }) => {
    const behaviorState = useBehavior(input);
    const unrelatedState = useUnrelated();
    return ...;
  };
At this point, we're not so worried if we need to hoist that one function call up to App and pass the hook's returned handle (behaviorState) back down to Foo and Bar at some point in the future.

If this example sounds trivial, I've packaged up 500+ lines of functionality in a single self-contained hook before, and that's all exposed as a single function that consumers can treat as a full encapsulation of all of that functionality, while not worrying if they need to hoist that function call up and pass the output down at some point in the future.

I'm not talking about calling the same hook in Foo and Bar (these are different instances), nor sharing local state sideways across different invocations of hooks, but rather having full confidence it won't be hard to re-anchor that hook upward in the component tree if need be.


You're right when it comes to hooks vs classes, that is a major plus. But this is not the aspect I was commenting on, I was specifically addressing the idea of custom hooks as a trivial replacement for Redux:

> One of the problems Redux is used to solve (as a global store) is peace of mind that you won't need to refactor large swathes of component trees to reparent state and callbacks that need to be shared or persisted across different subtrees as new business requirements arise

> Hooks (especially custom hooks, which are just a composition of other hooks packaged together as a single function) make the reparenting/hoisting/anchoring of state and callbacks trivial compared to other mechanisms, providing similar peace of mind that you're not boxing yourself into an inextensible component tree. (This is regardless of context; passing down props isn't a large pain point, and is often misguided to try to solve because it leads to importable components and hidden contracts.)

My earlier example was to show that re-parenting/hoisting when you replace a global store like Redux, with custom hooks using local state, is not straightforward, and unsafe except for very localized components where you shouldn't be using Redux anyway.

So you'll eventually end up doing prop drilling, or resorting to Context, or back to Redux; in the end hooks did not solve those issues as you proposed (other than the increased portability compared to classes, which you already have with redux or context).


The implication behind “we don’t use Redux, we use hooks” is that they’re using React’s Context API directly as a “store”. A component at the top level has some state, probably using the useReducer hook, that state is passed down using a Context Provider, and child components can access the data with useContext.

You can DIY a Redux imitation with hooks pretty easily but making it as performant as Redux is harder.


Indeed. IIRC redux itself tried that in an earlier version but reverted to a custom update / forced render mechanism due to performance bottlenecks with Context.


Yep. References with the details on what we tried to do in v6, and how we rewrote things in v7:

- https://github.com/reduxjs/react-redux/issues/1177

- https://blog.isquaredsoftware.com/2018/11/react-redux-histor...


They might be using context. The problem with that though is it will cause performance issues due to unintentional rerendering. Not something to worry about for a small app.

https://reactjs.org/docs/context.html


Are people just using "hooks" to mean "all new functionality in React that happens to be available to/as hooks"? To me it's like (actually very like) "oh we're doing that with methods". Uh. OK? Cool I guess? That's not informative.


Maybe. I really don't know.

There are some places where functionality is dependent on hooks.

React fast refresh won't work with class components. (useState is the hook alternative to class style state management and fast refresh is fancy hot reloading but with persisted state and only reloading modified modules/components).

There are few escape hatches only built as hooks mostly for improving performance issues which weren't previously available. But most of the hooks are basically reimplementation of features from class style components.

There are differences in how hooks work though. Hooks are executed as they are laid out.

Previous life cycle methods were dumped into a single useEffect hook.

It's easy to change hooks and introduce separation in how you build your logic is what I assume they might be referring to.

Instead of

<Consumer>

{({text}) => {

  return <div> {text} </div>
}}

</Consumer>

You can do

const [data] = useConsumer()

return <div> {data.text} </div>

It gets messy with a real complex application.

typing on mobile is sad.


Indenting by four spaces gives you preformatted text on HN. It's for code.


You're probably not far off. For the Redux case, that statement almost certainly means using at least one top-level Context Provider along with `useContext` in consuming components to replace or avoid the use of a Redux store and `connect`.


If you ask a random sampling of Redux users why they're using it, you'll get tons of different answers. Some people use it as a caching layer. Other people use it because their SSR solution requires it. Other people use it because they like the action-command pattern. Other people just use it because they see it as the "M in MVC" and consider it mandatory in their projects. Other people just use it because it's the default. Some people use it for more than one reason.

If something else comes along and fits one of those niches, it's no wonder people will get interested in it. It was the same with Apollo. A lot of projects replaced Redux with it.

And to answer your question: some people are moving to hooks because of useReducer. Others because of useContext. Others because they can create custom Hooks. Some because of third-party hooks. Others for a mix of those.


Normally they mean they are using React Context, useReducer and React.memo/useMemo.

Most of the people saying that are using that are suggesting it lowers the code complexity as in the code is easier to understand or follow.

They probably will also split up the application state in multiple React context, one for auth, one for settings, things that aren't depending on one another.

Especially, when you are application doesn't have to manage that much state yet. I think it's a reasonable approach to get started with it. You can always switch to something like Mobx state trees or Redux at later time.


I'd guess comments like this come from people thinking the primary complexity of Redux is the data store, rather than the "all changes to your state are represented as plain objects". The Redux FAQ goes into this: https://redux.js.org/faq/general#when-should-i-use-redux

Can't blame them missing the forest for the trees when you spend all your time writing the reducers and connectors vs the actions themselves.


Note that our new official Redux Toolkit package [0] simplifies most Redux use cases and logic, including eliminating the need to write action creators and action types by hand and allowing "mutating" immutable update logic in reducers. The React-Redux hooks API [1] is also generally simpler to use than `connect`. We're now recommending that people use RTK as the default approach for Redux apps [2], and I'm working on a new set of tutorials that will teach RTK and React-Redux hooks as the standard approach [3].

[0] https://redux-toolkit.js.org

[1] https://react-redux.js.org/api/hooks

[2] https://redux.js.org/style-guide/style-guide#use-redux-toolk...

[3] https://github.com/reduxjs/redux/pull/3740


Perhaps they mean instead of Redux, they are using Context + useReducer.


I can give my take on this.

Until hooks I used redux in every app because it was the default way to manage state. Now I use hooks, I have more tools to help me manage state (and also load things via useEffect), I find I simply don't need redux anymore. The only compelling reason for me to pull in redux now would be if I wanted a centralised cache, but the kind of line of business apps I mostly work on lately just don't need to cache things like that, so each route/page just reloads the data from the server.


If you want a lightweight cache, check out react-query[0]. It's a nice abstraction around fetching data, and it handles caching out of the box, as well as refetching data when the browser regains focus and a lot of other nice things.

0: https://github.com/tannerlinsley/react-query


Thanks, I'll check it out.

I find using apollo's graphql client library solves this reasonably well too, though the syntax is a bit clunky and knowing when you need to explicitly define a cache function or not is tricky.


This might be of benefit: https://blog.logrocket.com/use-hooks-and-context-not-react-a...

If Redux is a a vanilla-ish JS store and a Reactified access pattern, keeping the store but replacing the access pattern with React Contexts.

I am not a React expert.


This is replacing Redux with hand-rolled Redux. The pattern works, and is useful for very small apps, but get anything more complex where a state container is a useful thing and the tradeoff makes it pointless. Redux is extremely simple, very easy to test, and has excellent tooling. React-redux has necessary optimisations already written into it. And the boilerplate argument holds much less weight with hooks and redux toolkit.


We decided on the useReducer-hook instead of using a proper state library.

We did this since the thing we are currently building is essentially just a set of forms with a lot of client-side and server-side validations.

Of course, if we were building a large SPA we would probably reconcider at this point. It feels like we're already at the edge of what useReducer was designed to do and I do not think what we are doing now would scale cleanly to a larger project or a larger team.


saying "we use hooks" is a very broad term, a hook can do almost whatever you want. So I understand people that say that aren't being specific but it's assumed they're using a combination of hooks that would do a similar task like useContext, useReducer, etc


I can't say for sure what they mean, but I'd guess it's saying they keep shared state in a parent component rather than having a global data store for their app. The useState hook might explain their chosen wording.


You are correct. The variety of replies makes that pretty clear if it was in doubt.


[flagged]


I don't think that you'd be able to get a very clear answer from your average frontend developer on why their non-React framework of choice uses the state management pattern that it does. For that matter, most DBA's wouldn't be able to answer detailed questions about their chosen database's query optimizer, and most ML researchers wouldn't be able to answer questions about Numpy's choice of linear algebra algorithms. Sometimes people just need to do their jobs without understanding every nuanced choice all the way down the stack. Luckily, software libraries let us do that.


I feel going out of one's way to use a 3rd party library like Redux with React is a bit different than a database's internal query optimizer.

Quite frankly the fact so many developers are replacing Redux with hooks is proof it was never the right choice for their project in the first place.


Or that Redux _was_ a reasonable choice at the time, and that with the ecosystem changing and other options available, it's worth re-evaluating use cases and updating the decision.

See https://blog.isquaredsoftware.com/2018/03/redux-not-dead-yet... .


>Or that Redux _was_ a reasonable choice at the time,

Was it tho? The entire point of the post you linked is really about how "you might not need Redux" and for those who do, hooks can't replace it. If hooks can replace your usage of Redux than you never really needed Redux in the first place.


Personally, hooks have vastly simplified my code bases by giving me an out-of-the-box toolset for managing and fetching state. Not using redux means tons less boilerplate and actually means each "page" of my SPA's are totally decoupled from each other. (You can do this with redux by giving each page its own store but you need to be disciplined and refactoring can be tricky).


Why was redux chosen in the first place if it had tons of boilerplate, etc?


I'm not quite sure what the point of this quiz is. I don't know the answer to those questions, and yet I seem to have no trouble building very large react applications (with hooks) anyway.

I would also struggle with similar quiz based on the callbacks for class components. And those I actually had to deal with on a frustratingly regular basis! At least with hooks I can remain blissfully ignorant of what happens under the covers of useEffect().

Maybe the answers don't matter and this is a pointless exercise.


I'd definitely agree there's nuances about hook behavior that a lot of folks aren't familiar with. (Dan Abramov's mammoth post "A Complete Guide to `useEffect`" [0] ought to be required reading for all React devs, and a lot of the info in that post ought to be better integrated into the React docs directly.) That said:

- The "effect behavior runs bottom to top" has always been true about class lifecycle methods like `componentDidMount` / `componentDidUpdate`. So, nothing new there.

- Somewhat similarly, the aspect of new prop references causing some logic to run every time is not completely new, either - it's similar to how attempts to optimize rendering via use of `PureComponent`, `React.memo()`, and `shouldComponentUpdate` can "break" when the parent component passes down new callback or data object references every time.

- The complaint that "there's more stuff to learn" seems superfluous. If there was never anything new to learn, that would mean that the API was completely stagnant and that nothing new could ever be introduced. Yes, the ecosystem is in a somewhat uncomfortable transition state atm given that there's two different APIs that offer equivalent functionality, but "learning more stuff raises the barrier to entry" is not a particularly meaningful argument against hooks.

[0] https://overreacted.io/a-complete-guide-to-useeffect/


Agreed. At a certain point if experienced developers are struggling with the quiz it says a lot more about the quiz than the quiz-taker.


I thought the quiz was really missing the point. The whole point of reactive programming is to not worry about execution order.


A clear, concise & clever blog post.

Quizzing the community on its supposed expertise, is such an effective way to separate rhetoric from reality.


While I like the post and associated quiz, I feel like execution order is not the most important thing to understand with hooks, but it's the main theme of the quiz. If you're designing your components in an effective way, execution order shouldn't _really_ come into play. It typically only comes to bite you if you're manipulating some global state outside of React, or doing some direct DOM manipulation.

Questions 3 and 4 about anonymous objects and useRef are definitely the kind of knowledge that someone who aims to understand React and Hooks should focus on, though.


This was a great follow up. I completely agreed with the first post, and I couldn't help but think those critiquing it didn't quite understand what they were critiquing. It's rare that you're able to actually definitively prove that out, but these results are about as conclusive as it gets.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

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

Search: