14 ms·
Two custom React hooks
- culi 5y agohttps://usehooks.com/ https://usehooks.com/
- knuthsat 5y agoHooks are great! 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.
- HuntingMoa 5y agoreact-testing-library provides a helper to test hooks in relative isolation - renderHook.
- bayesian_horse 5y agoThere are special tools to "render" react components in node-based test runners. 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.
- gherkinnn 5y agoUnless you're talking about library-level hooks, there's rarely a need to test them directly. I'd mostly consider them implementation detail. Testing the components themselves is ample.
- loh 5y ago> how do people test this You can find the tests for these hooks here: https://github.com/Molecule-dev/molecule-app/tree/6e2456e216767b529c32d16a6952e1ff0858250c/src/hooks/__tests__ https://github.com/Molecule-dev/molecule-app/tree/6e2456e216... Is that what you're asking about?
- steve_adams_86 5y agoOpinion: Although you can test hooks in isolation, I find they tend to be fairly primitive and testing them in combination with components can be more useful. 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.
- bayesian_horse 5y agoI think even those examples border on usecases for more complex state management along the lines of redux, Zustand etc. To me, useState with a complex object is an antipattern. I'd much rather use multiple useState invocations. This eliminates the need for extendState and for handling functions and promises. Also, take a look at Redux Toolkit Query and React Query. Both provide advanced query state management based on hooks.
- maest 5y ago> To me, useState with a complex object is an antipattern. I'd much rather use multiple useState invocations. 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.)
- Jenk 5y agoThat scenario to me is a sign of a greater workflow than what should be in the view. Some other model should be handling that transactionality, not the view itself.
- tshaddox 5y agoAren’t multiple setState calls in the same tick likely to be batched and only result in one new render with all state values updated?
- steve_adams_86 5y agoBefore React 18, it was possible to get multiple renders depending on where your calls to set state occurred. Inside different event loops, async calls, etc would lead to multiple renders even if they all settled within a few ms of each other and before a render occurred. The batching algorithm wasn’t aware of each asynchronous context. In React 18 I believe this is mostly fixed.
- tomduncalf 5y agoThe extendState one confused me a bit as I just keep one “atom” of state per useState since Hooks were introduced (so you’d end up with foo, setFoo, bar, setBar). I hadn’t really considered doing it the way they are doing it (which feels more like the old this.setState API). How do other people use it?
- johnday 5y agoI agree. Though it's possible that it makes sense to have some ideas "unified" into a single state variable if only some configurations of the pair of values are sensible, and you want to keep both in lockstep - or if things trigger off of state changes in any of several values, and you want to modify several values at a time.
- hn_throwaway_99 5y agoThere are other comments here making the same general point, that extendState seems like a bit of a weird, and unnecessary, use case, and at first reading the blog post I agreed. 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.
- roastnewt 5y agoI use hooks the way you do for independent information, but when the states are related (like a userID and a userName for example), then it's better to have them as properties in the same object. If they tend to both change at the same time, that could cause a double re-render, as well as temporarily having an inconsistent state when one has updated but the other hasn't yet.
- TimMeade 5y agoExcellent! Love the extended state.
- bayesian_horse 5y agoIt's unnecessary. Just use individual useStates for each property. Makes it easier to type, too. Otherwise you might as well go with something like Redux.
- xixixao 5y agoRedux is far from the suggestion, and imho unless you can greatly benefit from the actions boilerplate its overhead is not worth it. As others mentioned useImmer is a more bulletproof solution then assuming your state can always be modeled by a single layer Object though.
- bayesian_horse 5y agoI too felt like that for a long time. Eventually I got when and why Redux is useful, even with the boilerplate. Also, modern semantics and utilities greatly help. Redux Toolkit, RTK Query, reselect and so on. 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.
- handrous 5y agoI like Redux because it's portable—you can easily rip it out of a React project and use it anywhere—and because it provides a really nice way to separate backend-talking-stuff from frontend-rendering-stuff, if you're dividing up dev tasks. Nice clean place to split off a library for whatever service(s) you're consuming. And with Typescript, it's not a bit unpleasant to work with.
- 5e92cb50239222b 5y ago
- enjoylife 5y agoI don't know what applications they are developing but an application only needing two custom two hooks, would be a quite trivial one. Not to mention the hook in the article extendState is basically bringing back the Component `this.setState` semantics but not much else.
- loh 5y agoThese patterns are used all throughout an email app built and released earlier this year: https://www.tricepmail.app https://www.tricepmail.app We'll probably open source portions of it as well eventually, but first we're giving it a huge UI overhaul.
- notpachet 5y ago> In the case of modern front end engineering and React especially, you can reduce everything down to two simple concepts... > Rendering the current state > Updating the state 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...
- loh 5y ago> Separate your view layer from your application state. 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/6e2456e216767b529c32d16a6952e1ff0858250c/src/App/Store 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/6e2456e216767b529c32d16a6952e1ff0858250c/src/API 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/6e2456e216767b529c32d16a6952e1ff0858250c/src/API/resource/thing/index.ts https://github.com/Molecule-dev/molecule-app/blob/6e2456e216... I may be misunderstanding your complaints though. I appreciate your feedback.
- notpachet 5y agoWhat I mean is that you're violating standard separation of concerns when you have components that themselves are capable of dispatching network calls and updating the state in an async way directly within the components. As a result, those components no longer have an instantaneous view of the universe, and that makes them harder to test, harder to reason about as isolated units of abstraction, and so on. > 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.)
- Waterluvian 5y agoPedantic nit: [ readRequest, requestRead ] reminds me of one of my least favourite nitfalls from Django: FileField and FieldFile. Yes, the names make sense. But human brains mash these things up so regularly that I never seem to be able to permanently remember what's what. I'm always triping on them. 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.
- depressedpanda 5y agoI would argue that object unpacking is superior unless you have something similar to useState, where you only return two variables, and you know you'll never need the second variable without the first (which in practice may be very difficult to know beforehand, as requirements change). 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()
- Waterluvian 5y agoI like the `extendState` concept. So far, most of the time, I find that modern JS is fine enough. I can absolutely see the benefit of abstracting this concept. But I'm always hesitant to layer on top of an API just for little improvements. The cost is deceptively high: you have to re-remember your custom API and others have to learn your custom API. setState({...state, foo: "bar"})
- andrewstuart 5y agosetState({...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"}))
- Waterluvian 5y agoThanks. Most importantly, though: why? Is there some race condition about updates or bulk updating?
- andrewstuart 5y agoBriefly - it is because React batches and executes setState calls some time in the future - thus you MUST use a function call to get a non-stale state. Refer to my more detailed explanation elsewhere in this thread.
- Waterluvian 5y agoThanks. I see the example in the docs shows this method. StackOverflow shows many people sharing the same. I’ve never had this problem so I’m probably quite lucky. But it makes complete sense. I’m surprised it’s not called out on the docs page for setState.
- andrewstuart 5y ago>> I’ve never had this problem so I’m probably quite lucky. 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.
- strogonoff 5y agoA nice approach of separating concerns is having the API layer provide its own, already typed hooks and hook-backed primitives such as `useThing()`/`updateThing()` (or `API.thing.useData()`/`API.thing.update()`, etc.), which under the hood can do whatever necessary (including using shared logic, API-specific access request handling) while providing common primitives for tracking progress, cancellation[0] and other conveniences. With TypeScript, you can ensure those hooks are typed appropriately and don’t require callers to annotate. 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.
- loh 5y agoYou can certainly compose a `useThing` hook (and others) for more encapsulation and functionality specific to said thing. 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.
- strogonoff 5y agoI prefer to generalize logic at another level of abstraction, but to each their own.
- null_deref 5y agoHave you thought about using react-query? The second hook is given freely by 'react-query', the first one is done little bit differently
- mattwad 5y agoSurprised at the negative comments already. I'm not sure half of the commenters use React on a daily basis. I've re-written these same hooks in various ways, as well as used ones written by others. This look great to me, personally! I've been using a weird implementation of useReducer() (most commonly suggested on SO) but extendState() is a more elegant solution. 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
- loh 5y agoThat's a good call. I'll change those to better names. 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/_e745872f9c566d0b7c6bdfce748f2ab4b809c0ca/src/hooks https://github.com/Molecule-dev/molecule-app/tree/_e745872f9... I'll add a direct link to that too. I really appreciate the feedback.
- tuan 5y agoIn the design pattern mentioned at the end, there are some application state that are stored in both `updateRequest` and `state`, i.e. the data that is returned from the update response (stored in updateRequest) and is used to extend `state`. Having the same data stored in 2 different states seems error-prone. Which one is the source of truth when the 2 states accidentally get out of sync ? For example, nothing prevents extendState() to be called again to modify the `state` to something that is not the same as the value from update response.
- loh 5y agoThe API response ends up being the source of truth, in this case. If you wanted to prevent the user from updating the internal state while waiting on the API, you could certainly do that with a check for `updateThingRequest.status === 'pending'`.
- nightpool 5y agoThe initial writing for "useAsyncExtendedState" said "Don't use extendState for everything! Use it only when you know you need to merge a partial state.". However, the example doesn't use "setState" at all. This feels like a gap or tension in the API design—if setState doesn't get used in practice, why force users to include it? Instead, I think the default react setState pattern is much better, since it allows you to easily set *or* extend your state depending on your usecase: const [state, setState] = useState({}); // replace setState({foo: 1, bar: 2}); // extend setState(currentState => ({...currentState, foo: 1}));
- loh 5y agoIn the examples, `extendState` is used because the API returns only the updated props when updating. 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.
- deleted 5y ago[deleted]
- flippinburgers 5y agoJust use redux.
- imbnwa 5y agoCould Hooks be construed as Facebook Engineering's attempt at modeling React as an Effect monad?
- thrwy_918 5y agoSomething I often want to do is write a custom hook with internal state that will be used by two or three components - and what I really want to do is have consistent internal state for that hook, regardless of which component its being invoked from. 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
- ngoel36 5y agoRedux is a game changer here https://easy-peasy.vercel.app/docs/api/create-store.html https://easy-peasy.vercel.app/docs/api/create-store.html
- acemarke 5y agoI'm biased, but I would strongly recommend using our official Redux Toolkit package rather than Easy-Peasy: https://redux-toolkit.js.org https://redux-toolkit.js.org
- ngoel36 5y agoCurious, why? (I admittedly know very little here)
- acemarke 5y agoBelated answer, but several reasons: - 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.
- chrisfosterelli 5y agoThis is what react context is for. You can create helper hooks to expose the context more conveniently. EDIT: I threw together a small example if it helps: https://gist.github.com/chrisfosterelli/2e523b4beae43f05624920b26bb55fa0 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.
- brundolf 5y agoI think everyone has their own implementation of usePromise; it’s weird to me that it isn’t included out of the box
- twic 5y agoOkay so if i have: 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');
- loh 5y agoYou can think of it very similarly to `useState`, where it returns a particular state and a function to update that state. 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' })
- goblin87 5y agoFor your first hook just do this instead: setState({ ...state, foo: 'updated' })
- andrewstuart 5y agoHow is extendState different to setState? setState already allows you to extend the current state. I don't get it.
- mlnj 5y agoBased on your other replies I think you got it already, but I'll just add it here for the sake of others. 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.
- andrewstuart 5y agoAs mentioned elsewhere, this is invalid code - a bug waiting to show itself - you should never do: 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.
- drdec 5y agoThe docs for setState say that if passed an object it will merge the current state and the object. So just do: setState({ a: 4 }) https://reactjs.org/docs/react-component.html#setstate https://reactjs.org/docs/react-component.html#setstate
- loh 5y agoThe docs you linked to are for class components (`this.setState`), not function components using hooks (`useState`). From 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-state-variables 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.
- lifeplusplus 5y agosimple few concise meaningful lifecycles methods to vague handicapped overlapping infinite hooks, really outdone in the name of progress.
- DangitBobby 5y agoI have a custom hook that I like to use called useLocalStorage which is just a wrapper around useState which first looks for existing state in localStorage. There is a defaultState argument if there's nothing in localStorage.
- emadabdulrahim 5y agoThanks for sharing your post. I want to highlight that react-query solves all of those problems very elegantly. You should check it out if you haven't already.