Two custom React hooks
blog.molecule.dev
blog.molecule.dev
Couple suggestions:
* the repetition of "read" `const [ readRequest, requestRead ] = usePromise(read)` makes it hard for me to keep these things separate. It would be easier if you had a real life example, like "get users" or something but even somethin like `const [ apiState, fetchApiState ] = usePromise( apiRequest )` would be better IMO.
* `readRequest.cancel` - sometimes I've wanted to cancel a request but it's not an error to show the user, but it looks like the view wouldn't be able to tell the difference from a regular http error
* `readRequest.reset(`error`)` could be another way to just reset a single property, instead of having to use brackets
* uh where is the code for the hooks? A direct link to code or even a library would be great
Also, you can call `cancel()` (with no arguments) if you don't want an error.
You can find the code for the hooks here: https://github.com/Molecule-dev/molecule-app/tree/_e745872f9...
I'll add a direct link to that too. I really appreciate the feedback.
Also, take a look at Redux Toolkit Query and React Query. Both provide advanced query state management based on hooks.
The issue with that is that sometimes you need to make transactional changes to your state (i.e. either update 3 variables together, or not update them).
That gives you 2 options: 1. bundle the state up in a complex single object with useState 2. extend the space state of your app to allow for mid-transaction rendering (i.e. only 2 of the vars have updated and the 3rd has not).
#2 feels a lot harder to me, tbh (although I do strongly dislike #1. and would like an alternative.)
In React 18 I believe this is mostly fixed.
There are exceptions I’ve forgotten but generally it solves this problem well.
Are you referring to the usePromise and useAsyncExtendedState? If so, it's okay to use those hooks independently. They work fine without each other, but they also work well together.
const [state, setState] = useState({});
// replace
setState({foo: 1, bar: 2});
// extend
setState(currentState => ({...currentState, foo: 1}));Your preference for only `setState` is definitely warranted. The source is available for this reason, and it's pretty compact. You can quickly remove the `extendState` portion if you want.
Another day, another React team coming to the belated realization that hooks are an inelegant solution to problems that have already long been solved. Separate your view layer from your application state. Get all those network calls the hell out of there. Relegate your components to simple stream transformers -- props in, HTML out. If you're doing anything other than that, your function components aren't really pure functions.
It's frustrating to watch an entire swath of the industry continually rediscover its own inadequacies year after year...
It is actually separated, not from React though because React will need the state (data) at some point anyway. We'll go into more detail on that in the next post. To touch on it briefly here, shared application state exists on its own at the top level of the app, as a composition of stateful hooks. See here: https://github.com/Molecule-dev/molecule-app/tree/6e2456e216...
Every possible API request also exists on its own outside of anything React (view). See here: https://github.com/Molecule-dev/molecule-app/tree/6e2456e216...
Or for a more specific example, see this API resource route index: https://github.com/Molecule-dev/molecule-app/blob/6e2456e216...
I may be misunderstanding your complaints though. I appreciate your feedback.
> Every possible API request also exists on its own outside of anything React
That's good, but I would take it once step further and disallow any React component from directly invoking those methods.
Of course you're always going to need to do something asynchronously when the user clicks a button or what have you. But in my opinion, it's a lot more maintainable to have that just be an event that the component fires, and then have something else listening for that event out-of-band (and then sending the network request / updating the state to say "connecting" or "request failed" and so on). What happens in async land is not really a pure component's business.
(I know I'm definitely out of lock step with the current React ethos on this, so permit a crusty neckbeard his pet gripes.)
The React ecosystem has spent the last few years trying a model where the application layer separated from the view layer with a pure functional state management solution called Redux. The overwhelming response? People didn't like it.
Decoupling systems is a trade-off. Pull your network requests out of your components and you have two bits of code that are easier to test. Indirectly, the component is still going to call that code, and it's up to you to manage the complexity of that indirection.
Not every application needs separation of concerns, and in many, colocation of concerns reduces the cognitive burden, because you can reason about components in isolation. To me, that's a more powerful guarantee than a function being strictly pure.
I like (and use) Redux on a daily basis, and I don't think it's a sensible choice for lots of React apps. Maybe the overwhelming rejection that I've witnessed is people discovering that they used it for stuff they shouldn't have.
It won the Flux wars, but there are state management approaches based on paradigms other than flux. There's at least the model that Recoil/Jotai uses (atoms?), the model that MobX/Valtio uses (mutable observable state), the model based on state machines (XState), the model heavily using React context (Constate), etc.
The churn in the frontend ecosystem reminds me a lot of fad dieting: people have never really experienced working in a paradigm that enables them to stay healthy through regular diet and exercise over the long term, so they turn to the latest shiny gizmo hoping that this time it will be different.
For any project that I'm responsible for, I don't feel comfortable designing around a paradigm unless there are some guard rails to prevent developers on my team from accidentally tying the code in knots. How enjoyable is this for the developers? Are they sprouting angel wings and playing the lyre as they write code in iambic pentameter? Probably not, no. They have less freedom of motion than if they were left to their own devices.
This is a larger debate: where to fall on the spectrum between unadulterated developer bliss and having an application that is still maintainable in 5 years. I don't put much stock in developer bliss, but I do appreciate that the sword cuts both ways.
Redux is still by far the most widely used state management tool with React apps (my estimates are around 45-50% of React apps use Redux), and we try to give clear guidance in our docs on when it does and doesn't make sense to use Redux.
FWIW, we get highly positive feedback on a daily basis from users who tell us how much they love using Redux Toolkit.
Resources:
- https://blog.isquaredsoftware.com/2018/03/redux-not-dead-yet...
- https://blog.isquaredsoftware.com/2021/01/context-redux-diff...
- https://blog.isquaredsoftware.com/2021/05/state-of-redux-may...
- https://blog.isquaredsoftware.com/2021/05/learn-modern-redux...
- https://blog.isquaredsoftware.com/2017/05/idiomatic-redux-ta...
- https://redux.js.org/tutorials/index
- https://redux.js.org/tutorials/essentials/part-2-app-structu...
Using the redux-toolkit and the hooks, with none of the additional junk is a great experience.
I have respect for what acemarke writes above and I think acemarke is right that redux has been overused a whole lot.
A bit sad that immer was used (a bit too much magic for my taste), but I can understand the reasoning, and if you just accept it, it's ok.
Thank you and all the other maintainers for your work!!!
- Too much "magic" as you said
- Proxy-wrapped data values can be harder to inspect in the browser's debugger
- Seeing "mutating" code may be confusing
I understand those, but from my perspective the benefits of Immer far outweigh any potential negatives.
Ultimately all Immer does is track attempted mutations to nested data, and replay them to create the immutable updates as if you'd done all the nested spread operators yourself. There were dozens of immutable update libs in the ecosystem already (I had listed most of them in my Redux addons catalog), and accidental mutations have always been the number 1 cause of bugs in Redux apps. So, a better way to write immutable updates was clearly valuable.
Immer drastically simplifies all that update logic:
- Removes all the extra object spreads and slices and concats you would have had to write previously, so the code is much shorter and easier to read. It's also more clear what the actual intent of the state update is.
- It also completely eliminates accidental mutations _in_ reducers, and also _out_ of reducers by freezing the state.
So, shorter code, easier to read, and eliminates the #1 cause of Redux bugs.
The tradeoffs are a couple KB of bundle size (which is quickly paid for by having shorter reducers), and needing to understand that Immer is there and doing this "magic" for you.
The one still slightly nagging annoyance is that inspecting Proxy-wrapped data in a debugger is painful, but Immer does supply a `current()` util to extract the plain JS data if you need to, and we document doing that:
https://redux-toolkit.js.org/usage/immer-reducers#debugging-...
So, as with all tools Immer isn't 100% perfect, but it was absolutely the right choice to include it in RTK by default.
But as I said, I still highly recommend Redux Toolkit. After dealing with the code using MobX and the complexities brought by its reactions, I would take Redux anytime. Immer is just a small unfortunate detail, but I can live with it.
You still have to understand Redux's weird boiler plate concepts - actions, action creator functions, reducers, thunks, ... - to understand and use Redux toolkit. Redux is broken from the bottom up.
Note that I would never (again) use thunk - using such side effects leads to unmaintainable code, at least in my experience.
I can't speak to his code contributions, but in terms of documentation, tutorials, and community engagement, most open source projects would be lucky to have similarly prolific contributors.
FWIW I _have_ written actual code in the libraries too :) I wrote React-Redux v7 by myself, shepherded the design of our hooks API in 7.1, and did all the coding work for v8. Also created Redux Toolkit, added a couple additional APIs, and a bunch of other stuff :)
These are all obviously _very_ imperfect proxies for actual usage stats, but they're all that I see available to work with.
FYI my last attempt to estimate "React state management market share" was in a Reddit thread earlier this year, with download numbers and caveats included:
https://www.reddit.com/r/reactjs/comments/lcgqnd/state_manag...
The biggest thing I miss in react ecosystem is decent redux ORM. https://vuex-orm.org is just so great for so many use cases (agree that it might an antipattern in many situations). Is there any chance that https://github.com/redux-orm/redux-orm, which was actually what inspired vuex-orm, would get more love from anyone to become an actively maintained library?
Thanks
At the time, the main benefits to using Redux-ORM were easier immutable updates for items in the store, keeping items in a normalized state structure, and managing relationships between items for both lookups and updates.
Since then, Redux-ORM has changed maintainers and had its API tweaked. Meanwhile, we added a `createEntityAdapter` API [0] to RTK, which handles normalization and some CRUD-style updates to the data. Even without those CRUD helpers, you can still do `state.entities[itemId].someValue = 123` thanks to Immer, so immutable updates are taken care of.
At this point, the one remaining thing that Redux-ORM really handles that RTK doesn't is that specifically relational behavior - ie, given a `Post`, look up all its `Comment`s by an array of IDs or something. You can do a decent amount of that with selectors, but yeah, Redux-ORM's API here is nicer.
We haven't tried to provide any of that ourselves because this becomes a much more complicated and app-specific topic. But, I'm always open to suggestions for APIs and use cases. If you have ideas for things that we should consider adding to RTK, please do file an issue so we can discuss details!
It won't ever be a one-size-fits-all solution and unfortunately, Redux will have to live with the legacy of plenty of users finding that out the hard way.
That being said, I think Redux Toolkit is a great improvement to the vanilla experience and I'm always impressed by how much I see you out and about on the internet weighing in on these kinds of threads. Inspiring stuff!
That's not a bad thing (and it's better than the alternative of not caring for design patterns whatsoever). It's part of the learning process, and since there are so many beginner developers who are using React, their perspective is much 'louder' than in other dev ecosystems.
You can see this in the absurd amount of introductory-level React content posted to Medium, DevTo, Twitter, etc. This has bred a very strong 'follow-the-leader' culture where, when the one person is the room who _does_ know what they're talking about makes a statement, others will repeat it verbatim without understanding its nuances due to a lack of experience/context.
Redux suffered heavily from this. New React devs in 2017 were faced with mountains of tutorials which all used Redux. Many of these tutorials were written by other newbies. Your mental model of React dev was then shaped around Redux. Therefore you would put everything you could into the Redux store, which is a bad idea -- you usually don't need form state in there, for example.
Then some React thought-leaders saw this problem and inadvertently created a counter-movement by raising how Redux was overkill for some use cases, which was misconstrued as 'you shouldn't use redux _at all_'. The pendulum has been swinging back and forth ever since.
Yes, not every application needs separation of concerns. But some definitely do. It's not black and white, and that unfortunately means there's no definable 'best practice' that can be tweeted or blogged about and followed blindly -- it just has to be learned from experience.
Or maybe more accurately, discovering that it's often simpler to separate by concern at the component level, than at the app level.
We all ride the pendulum until we find the shade of grey which works best for us.
Why's that? IME it is usually simplest to just put everything in redux. For example, if you have a form under some kind of tab navigation thing, you'd ideally want the form state to be preserved even when they tab out and then back in. Putting it in redux means you don't really have to think about it
You'd still preserve on tab change just as easily, while using just reacts mental model.
Like, I don't want _everyone_ to use Redux. I just want people to understand "here are the strengths, weaknesses, and use cases of tools X, Y, and Z, and when it may make sense to use them".
Honestly, we Redux maintainers (Dan, myself, Lenz Weber) have had to spend far more time telling people when _not_ to use Redux than we have trying to advertise the library :)
When I rewrote the Redux docs tutorials last year, I made it a point to put "When should I use Redux?" right up front on the first page:
https://redux.js.org/tutorials/essentials/part-1-overview-co...
but that sadly can't have any effect on the thousands of existing tutorial pages or low-effort posts that are out there already.
I've been reading "blue" DDD book (by Eric Evans) and "red" book (by Vaugh Vernon) and that was a completely "my whole life was a lie" type of experience and relief at the same time. It's just so great to have the principles of who to structure the code. It, by definition makes, your codebase structure meaningful. Because it's structured according to some common knowledge, not your random thoughts at the time you were writing code.
I was surprised to find so little DDD react sample codebases. Let's say for backend there is huge amount of samples, i.e. https://github.com/kgrzybek/modular-monolith-with-ddd . For react/frontend I have bookmarked only https://github.com/talyssonoc/react-redux-ddd/tree/master/sr... and few more, but those others does not meet the optional criteria i like really much - at the highest (or at app) level all codebase need to have folders app, domain, infra and ui. Simple rule, but simplifies life a lot.
So my question is - is DDD for some reasons not very applicable for app frontend development. Or it just never became popular. Or maybe DDD is popular amongst react developers, just I am not aware of this.
Many thanks for any ideas and comments!
Remember: components using hooks are pure functions! That's what makes them so effective.
https://overreacted.io/algebraic-effects-for-the-rest-of-us/
Yes, and given the same outputs provided by props + hooks, components will return the same value. That's why you can write hooks functionally, and why we can do away with class-based functions entirely. With the exception of refs, there is no actual internal state that your component needs to deal with.
The necessity(!) to wrap each and every basic JavaScript API (useEffect v. setTimeout, useLayoutEffect v. queueMicrotask, useInterval and usePromise v. using Intervals and Promises) does not at all showcase the "expressive power" of React.
It was fun while it lasted but it's time to let go. React's become JQuery
useEffect works by running on each render (no dependencies) or running when rendering and dependencies changed. Where does a timeout factor in?
useLayoutEffect also doesn't work based on the microtask queue - it uses React's internal scheduler, which I would imagine makes sense in the context of the library. Replicating it with queueMicrotask is probably not a trivial task.
React deliberately ships with only a few hooks, more or less the fundamental ones that you cannot yourself implement. They left the rest up to the community to explore, because it is very difficult to predict how a new paradigm will be used. Hooks isn't just useState and useEffect, hooks are a way of writing reactive logic.
It is very impressive that the community is able to produce new tools that simplify annoying problems like managing asynchronous state with just a single function, in a way that can be more-or-less dropped into any existing component.
This is insane. You shouldn't need to download useConsole from npm.
What? I don't think you understand hooks very well. You can already use promises with hooks by creating them and then calling `setState` however you want in the callbacks to deal with pending/success/error. It's such a common pattern that the community has developed a `usePromise` hook that abstracts away all that logic for you, meaning that you can treat any promise as a stateless value to be consumed by your components, one that will automatically manage its state for you and guarantee your UI is consistent without any additional effort. I never have to deal with managing asynchronous state ever again! That's what a good library does: allow you to build reusable components that abstract away complexity. Hooks let you abstract away state management, the bane of UI development.
I can tell you from experience dating back to Backbone and Marionette that it has never been so easy to deal with async state before. Coupled with Typescript, I also get the guarantee that I've accounted for every possible case (success, pending, error). It's a zero-cost abstraction.
What's your experience with hooks?
Your last statement is completely wrong, though.
ReactJS/hooks are not zero-cost abstractions. They impose a cost both in terms of real performance as well as cognitive complexity.
And when you run into performance issues you're almost certainly confronted with this pattern of escalation (assuming you've done nothing wrong):
1. Litter your Code with calls to useMemo/useCallback.
2. Re-evaluate your business needs because you've exhausted the hooks "primitive".
3. Iteratively and painstakingly force more and more logic out of ReactJS' scope until you've met your benchmark targets.
Following along quickly requires intricate knowledge of poorly documented but overwhelmingly complex ReactJS internals alongside(!) whatever knowledge of JavaScript internals is required.
You have to have and process 2 complex, unique and outright byzantine models in your mind simultaneously.
Compare this to Angular(!) which lets you use standard web APIs, additionally exposes useful standard(!) primitives, and(!) provides you with straight-forward(!) opt-out mechanisms.
Yet here we are, quite literally downloading useInterval from npm 40,000 times a week.
The fact that hooks like usePromise and useInterval can be trivially written on top of standard web APIs and guarantee that your component hierarchy will update consistently without dealing with internal state is a groundbreaking achievement. I think you are focusing too much on the fact that hooks like usePromise exist, instead of on what they enable.
Have you ever used usePromise, and seen how it turns a stateful object into a stateless one? It has never been so easy to encapsulate asynchronous state.
- The ensuing paragraph suggests you are conflating Angular with AngularJS. Angular is build on top of proper, standard FRP and stream processing primitives. It is "reactive".
- Angular is in widespread use in the enterprise world.
- The reason ReactJS is much more prevalent everywhere else is it has no barrier of entry. None at all. This, however, is irrelevant with regards to the issue I am raising. I am complaining about the complexity ReactJS dumps on working professionals.
- I am not advertising Angular. I am telling you that EVEN Angular manages to suck less.
> The fact that hooks like usePromise and useInterval can be trivially written on top of standard web APIs ...
Again, not: "can". "Must"!!
Unless you mock a hook at the module import level (which is hacky), you lose the easy testability of your component. That's long been bothering me about hooks, although I use them plenty.
> If you're doing anything other than that, your function components aren't really pure functions.
Nobody claims they are pure functions. They aren't - but that doesn't really matter. Its an API choice that encourages productivity while still having tools for the abstraction that you are advocating for.
State hooks can be seen as a sibling of props - both are inputs to the component, but whereas props "push" data into the component, hooks "pull" data from somewhere else. This _is_ separating concerns - when a component needs data that isn't inherently something the calling parent owns, then making it a prop would just be forcing that component's concerns up the tree. Often it is better for it to just be an impl detail.
I don't find the article here super compelling, and as a general practice it is often better to author your own custom hooks that encapsulate things like the underlying HTTP request away so that your component is only dealing with its own concerns, but functionally there's no difference and its just a matter of how you split up your code.
What other costs are there besides boilerplate?
Let's say I need a single bit of state in my component. If I store that in a local `useState(false)` and update it with `setState(newValue)`, it is extremely easy to write, and also follow what is happening and how it is happening.
But if dogmatically say that this is poor encapsulation - this state should live in some Redux store, with actions and action creators, reducer functions, etc. there is inherent complexity, and I think in the case of a single bit used in a single place that it is pretty easy to see that the complexity is disproportionate to the actual needs at hand.
But the way I see it is a sliding scale - your state starts to become more complex? or maybe how the state is updated requires some additional business logic? Maybe you want to start using that state in different places? Any of these reasons can be cause to introduce abstraction, but my preference is to introduce the smallest abstraction that achieves the benefit you're after.
Sometimes this abstraction is hoisting state up - now your component doesn't manage its own state, but rather it communicates with its parent via props+callbacks. Now its a little harder to follow (need to consider this component, its parent, and how the prop/callback are being used), but for that complexity we now get to share that state with sibling components.
Sometimes this abstraction is moving it to a reusable hook. Now you have a similar scenario where to understand how things are working you have to understand the component and the hook, but for that cost we can now share business logic.
Sometimes that abstraction is moving it into a redux store, with all the boilerplate that goes with it. For the right use case this is not a bad thing, but I would only want to do this if I'm getting a proportionate amount of value for the increased cost.
I now really dislike hooks because of the amount of implicit behavior and useEffect poisons the codebase to add even more implicit behaviors/side effects to your application.
They make the rendering behavior really difficult to reason about. I caught a bug where you had to click a button twice and the reason why it happens is extremely difficult to reason about: https://stackoverflow.com/questions/58106099/react-onclick-n...
In my opinion what we need is an explicit state machine of the rendering of a component like vue’s component lifecycle. And these hooks we are using for state management we should instead use xstate to make the state transitions of a component explicit. React hooks (and redux and react-query) are bad (in my opinion) because they are easy to write with and hard to reason about.
My gripes w/ Redux is that its pattern is clearly better suited to a runtime with more to offer functional programming than first-class functions, hence all the boilerplate and effort to abstract all that boilerplate, as well as deciding how to pick how you'll get to granular updates (which is where, from where I'm standing, React Hooks' design starts).
But in comparison to React Hooks, its vastly superior for affording clear separation of boundaries in an app.
What do I mean by that? The applications that I work on do a lot of composition of apis, reshaping the data and caching on the frontend since our backend teams are micro services teams. What happens is that our codebase needs to define multi api workflows and data transformations which we define using redux sagas or react query (we either adopt one or the other for the codebase).
The problem with the react query way of doing this is that the query has its own built in lifecycle it goes through stale -> fetching -> loading -> success/fail this is implicit behavior that you don’t have visibility into why it is making a transition. It becomes very painful for large workflows of api compositions to debug this kind of implicit behavior.
Redux sagas on the other hand is explicitly defined. We have these channels of communication over which messages are defined (actions) which cause some explicitly defined behavior to happen (sagas, reducers). Now why is this problematic? In a large codebase of micro services transactions we are dispatching actions in response to a parent action forming an explicitly defined implicit behavior.
So for example with react query if I am debugging an unexpected state in prod for a bug I have to reason about the way react query is processing a query in order to debug it due to the implicit behavior. In redux sagas world I have to reason about the implicit state machine of actions, sagas and reducers that a workflow kicks off.
This is why I don't like these two styles of organizing the application state. It becomes difficult to reason about the behavior but the redux/redux sagas way is easier to reason about because at least we explicitly defined the implicit behavior.
Also make sure to note I am talking about a complex web app on a micro-services api (frontend does a lot of api composition work), I am sure these are elegant solutions in the right context.
You may want to consider whether you're doing the back end devs' work for them. :) I'm a FE dev working with a lot of microservice back ends for the last 3 years, and a realization that I had early on was that the back end devs were reducing the back end complexity by pushing it into the front end. Whenever I find myself writing transaction logic for an operation that cuts across multiple services, I suggest we use a backend for frontend (BFF) service that performs those transactions instead. There is an incredible amount of complexity in the front end already, and things like read/write transactions and data consistency are not front end concerns. At my current gig we use an event driven saga architecture[0] (inspired by but no relation to redux-saga) to keep multi-service transactions out of the front end, and it just feels like everything is where it should be. Instead of using a front end saga, the front end sends a graphql mutation to a back end saga and gets back a transaction id, which it can subscribe to and get status updates on the transaction (ie. state and errors). It wouldn't make sense to have your back end devs manage the state of a dropdown, so why are they always asking us to manage their data transactions? :)
[0]: https://github.com/social-native/kafka-sagas (this is a purely back end library; the front end queries the saga state with plain graphql operations).
I think it helps to not think of them as "just a way of letting you write all your components as functions". That was always a red herring; even before hooks, React "functional components" were impure stateful objects at runtime. Only your part of the code was ever kind-of pure. It was an illusion, and Hooks just pulled back the curtain.
I think of Hooks as more like a DSL for declaring externalities, which just happens to take the form of a series of function calls. They're clunky for building complex memoized value-graphs [gazes longingly at MobX], and complex useEffect behaviors can get really hard to follow real quick. But for building average web apps - where 90% of the time you're either pulling in standard externalities or defining little pieces of primitive state - they make things really ergonomic. They also enable some fancy optimizations and scheduling on the React side, from what I understand. Overall I think they were the right move for a for-the-masses UI framework.
That's exactly right in my opinion. Hooks are the place to declare stateful and effectful (aka impure) logic. Using a hook is an equivalent signal to using a Future, or an IO in other languages like Scala.
Often combinations of useState and useEffect are best written as a custom hook to reduce noise in the containing component. An easy example can be seen in something like
const [ name, setName, isValid ] = useValidatedState(state, validationFunction);
Seeing that you can easily imagine how it would be implemented: const useValidatedState = <T>(initialState: T, validationFunction: (val: T) => boolean) => {
const [state, setState] = useState(initialState);
const [isValid, setIsValid] = useState(validationFunction(initialState);
useEffect(() => { setIsValid(validationFunction(state)) } [state]);
return [state, setState, isValid];
};
Almost always if there's a useState that has an associated useEffect, it should be turned into its own custom hook.Dan Abramov has a nice write up on the pitfalls of mutable this: https://overreacted.io/how-are-function-components-different...
With all that said, I would still say I'm not really a fan of hooks. I more see them as a necessary evil than a great feature. I try to use them as minimally as possible.
However, the issue is that the traditional division of concerns is more difficult to maintain in today’s web apps. Compared with web pages 20 years ago, web apps today are dense, interactive and complex. You might have dozens of UI components in a single page, each with their own piece of state, business logic and styling. Moreover, state, business logic, and presentation are oftentimes tightly coupled by design: eg, dragging this slider changes its shading using a complex algorithm.
Therefore, it’s becoming more advantageous to decouple individual ui components, each with their own state/logic/styling, than it is to, say, stick all of the state your web app deals with in a single place.
In other words, it makes sense to encapsulate all that code related to that crazy slider in one place, even if that includes state, styling, algorithm, etc…
Not a fan of the negativity in this post (I use hooks all the time) but the advice to separate concerns about managing state and rendering the state is GREAT for ANY architecture.
setState({...state, foo: "bar"}) setState({...state, foo: "bar"})
The code you presented is wrong - it won't work the way you expect. Or worse, it might work the way you expect, by chance.If you wish to use the previous state you must do it this way:
setState(prevState => ({...prevState, foo: "bar"}))
Is there some race condition about updates or bulk updating?
Refer to my more detailed explanation elsewhere in this thread.
Possibly, but more likely you've had weird bugs that never quite made sense and eventually things seemed to work so you moved on, but what was actually happening was a stale state problem. It's hard to write big React apps using the wrong way to udpate setState without bugs.
The second hook is given freely by 'react-query', the first one is done little bit differently
However, it "clicked" with me when I saw how they use their usePromise hook (to me it seems like their useAsyncExtendedState hook is only really useful in conjunction with their usePromise hook). That is, it is extremely common in React to call some remote API, then update the state when the Promise resolves. You also have a common set of things you want to handle: showing that the request is still pending, handling an error, updating a part of the state with some subset of the response object when the Promise resolves, etc.
It's very possible to do this in React without these custom hooks, but you'll usually find you have top-level state variables in your component that mirror the success/error/pending states. You need to do that all over the place.
Using these custom hooks makes it easy and consistent to make these remote calls but handling the stuff about the remote Promise request is essentially "contained" by the result of usePromise. Seems like a really nice, clean way to get consistency across remote calls in all your components.
setState((previousState) => ({...previousState, foo: false}))
({ contact: { address: "" } })
as opposed to
({ ...prev, contact: { ...prev.contact, address: "" } })
Given the way hooks are structured, if I want to use both `setState` and `extendState` in a `useCallback`, I'd need to pass both to the dependency array, which is just extra effort (which defeats the purpose of the syntactic sugar imo).
const shallowMergeReducer = <T extends {}>(state: T, action: Partial<T>): T => Object.freeze({
...state,
...action,
})
It ends up looking a lot like the "setExtendedState" function in the article.Often I'll throw in a few helper functions in a useMemo, too, and then provide the whole thing through Context. It's like a mini-Redux for some section of the app.
Compared to approaches such as `setAsyncState(API.thing.get<ThingState>())`, which do seem clever, the former approach may be a bit more concise, and I’d argue more predictable (if I see a `setState()`, I’d rather not have to do a double take and reason whether or not it actually sets state where it says it would).
[0] The cancellation approach in the example in this article rubs me wrong, because the update request is not (and can’t really be) actually cancelled, so the thing may be updated without GUI state knowing.
We actually do this elsewhere as necessary, like with the top level `Store` component. For the most part, it depends on the situation and how concise/redundant (or not) you want to be. In many cases it actually ends up being more concise overall to simply compose `setState(readThing(id))` as needed, instead of creating a specific method for every possibility.
const read = (id: string) => API.client.get<State>(`things/${id}`).then(response => {
return response.data
})
const [ readThingRequest, readThing ] = usePromise(read)
And i want to read two things from the server: const first = readThing('alpha');
const second = readThing('beta');
Then both will update the same readThingRequest, right? So the information in it will vary according to which one returns first or something?This feels like slightly the wrong scoping. Either let me call the wrapped function multiple times, and get multiple metadata objects:
const [firstRequest, first] = readThing('alpha');
const [secondRequest, second] = readThing('beta');
Or push the wrapping down to the calling of the function: const [firstRequest, first] = usePromise(read, 'alpha');
const [secondRequest, second] = usePromise(read, 'beta');With `useState`:
const [ firstState, setFirstState ] = useState('alpha')
const [ secondState, setSecondState ] = useState('beta')
With `usePromise`: const [ firstPromiseState, readFirstThing ] = usePromise(read)
const [ secondPromiseState, readSecondThing ] = usePromise(read)
To initialize the promise state similar to `useState('alpha')`: const [ firstPromiseState, readFirstThing ] = usePromise(read, { status: 'resolved', value: 'alpha' })
const [ secondPromiseState, readSecondThing ] = usePromise(read, { status: 'resolved', value: 'beta' })We'll probably open source portions of it as well eventually, but first we're giving it a huge UI overhaul.
This also brings me to something I've been wrestling with a bit: Array unpacking allows you to pick your own variable names, but you also generally have to unpack everything if you want some later variables. Order matters. I find this is not very self-documenting and is less fun for autocomplete.
Object unpacking lets you pick and choose what pieces you need from the hook's interface, and they come pre-named, which is good but sometimes bad.
I think I'm settling on Object unpacking being the better pattern for my tastes.
Object unpacking also allows you to alias the variable names, though it's slightly more verbose: `const { foo: newName } = someThing()`.
You can also choose to forgo unpacking entirely and just use the natural namespacing returning an object would provide:
const thing = someThing() thing.foo() thing.bar()
This obviously doesn't work with vanilla hooks, but is there a way to achieve this pattern? It seems like it would be so light and fast compared to a heavier solution
EDIT: I threw together a small example if it helps: https://gist.github.com/chrisfosterelli/2e523b4beae43f056249...
It's slightly overengineered in that not everything has to be in separate files; I just reduced this from an an existing example that was more complicated and had this file structure already.
- I created Redux Toolkit :)
- It's also "official", ie, from the actual Redux team (myself, Lenz Weber, Tim Dorr)
- RTK simplifies common Redux patterns, but tries to stay "typically Redux". It doesn't hide the fact that you're using Redux. Libraries like Easy-Peasy and Rematch add additional levels of abstraction, to the point that it doesn't even look like Redux any more. I can understand why that might be appealing to some folks, but I think it's too much abstraction.
Although, when I see code snippets on the internet I always wonder how do people test this.
The fact that dependencies are declared as imports and all the logic is inside the component function, it feels like you have to shuffle things around just to make the thing runnable outside of React.
Of course, you need to write your components in such a way that they are easy to isolate. You can also make use of composition and maybe even mock out components.
Testing the components themselves is ample.
You can find the tests for these hooks here: https://github.com/Molecule-dev/molecule-app/tree/6e2456e216...
Is that what you're asking about?
In isolation, unless you have very complex hooks, it’s a bit like you’re testing react or JavaScript themselves. When testing a component which uses the hook, I find you get to test the behaviour and expectations a little better - it’s close to a real use case.
setState already allows you to extend the current state.
I don't get it.
Setting a state is assigning the entire state object in this case:
setState({ a: 1, b: 2, c: 3 })
The next time i want to update that state (not replace it completely), I do: setState({...state, a: 4 })
or
setState(prevState => ({...prevState, a: 4 })) `for the sake of brevity, the extendState just hides the boilerplate away:
extendState({ a: 4 })
It's just a matter of convenience to me too. I just call it `updateState` in my projects. setState({...state, a: 4 })
If all extendState is doing is concealing the underlying function call, it's saving very little boilerplate and adding the cognitive load and potential issues related to the wrapper. import { useState } from 'react'
function App() {
const [count, setCount] = useState({ a: 0 })
return (
<button type="button" onClick={() => setCount({ ...count, a: count.a + 1 })}>
{count.a}
</button>
)
}
Cognitive load adds over time and I think it's so much simpler to break down such loads into smaller pieces that are documented well and abstract away the complexities/syntactic nuances. In short I keep forgetting to spread the previous state and that led to bugs for me.Edit: Yes, you are right about the stale state and that was one of the primary reasons for me to not make it a pattern everywhere.
It MIGHT work, but it is still incorrect code - you must always use an update function if you are updating state, it's not optional.
Let's put it another way to be clear. This code is using stale state, don't use stale state:
<button type="button" onClick={() => setCount({ ...count, a: count.a + 1 })}>
The example you give above is incorrect code - it does not work the way you think - you need to update your understanding of React. React batches the useState calls and executes them later - so you must be very clear about the data you are using in the useState function - picking up the state value from the enclosing code gives you a state value that you cannot trust.The ONLY reliable to way actually get the previous state is to use a function as the argument to setState.
In he example above, you think you are getting the current value of count, but it might be stale - this is critical to understand in React and if you don't understand it then you'll be fighting weird bugs forever.
If you wish to use existing state, when you pass an update function to setState then React ensures that the previous state argument passed in is in fact the previous state.
i.e:
<button type="button" onClick={() => setCount({ ...count, a: count.a + 1 })}>
should be: <button type="button" onClick={() => setCount(prevCount => ({ ...prevCount, a: prevCount.a + 1 }))}>
I think that's the correct brackets but HN is not an IDE.The rule is simple: if you are updating state, ALWAYS pass an update function.
The reason I have an issue with the topic of this HN post is that updating state is critically important in React and you should understand how it works and do it correctly, with a function call that uses previous state. useState with a function call is not boilerplate to be abstracted away, it's a simple and unambiguous way of writing React code and is central to writing React apps correctly - don't hide this.
setState({ a: 4 })
https://reactjs.org/docs/react-component.html#setstateFrom React's "Using the State Hook" docs:
> However, unlike this.setState in a class, updating a state variable always replaces it instead of merging it.
https://reactjs.org/docs/hooks-state.html#tip-using-multiple...
The `extendState` method gives you the same convenience of `this.setState` but with hooks in function components instead of class components.
I was replying to the poster who was using:
setState({...state, a: 4})
and so I assumed they were already operating in a class component.Also, the setters are usually passed around all over the place, so no, I don't think the point is centralizing component state at all.
> It's not mutable during the render function
I don't think that's the point at all when people talk about mutability. To me mutability is just a proxy for "lacking access control", which again goes back to the point about centralizing state updates. So arguing about the technicality of what is mutable and what is not is pointless.
As others mentioned useImmer is a more bulletproof solution then assuming your state can always be modeled by a single layer Object though.
Actions make state more debuggable. Selectors make rendering more performant and often help structure complex rendering logic. Also, Redux is a way of not having to define asynchronous control flow by composing components, which can get really messy.
Granted, it's not necessary for every app.
And with Typescript, it's not a bit unpleasant to work with.