11 ms·
Why Do React Hooks Rely on Call Order?
- danabramov 8y ago(I edited the post title to include “React” before the “Hooks” to disambiguate. Might be worth editing the submission too!) Hope you’ll enjoy reading it.
- strife25 8y agoJust want to say that I've been loving your posts so far. Well written and I love the perspective of exploring _why_ an API is designed the way it is instead of trying to focus on explaining why <insert library> is the best for <insert language>. There are only a handful of blogs I've read over my career that I feel like focus on solving real application development problems, exploring trade-offs, or digging into why an approach was taken and these posts are of that caliber. Those topics help me in the problems I encounter frequently in my day job and make me a better engineer. Thank you for the time and effort you're putting into this.
- rattray 8y agoThanks, this was fun. Enjoying your "unofficial" posts.
- politician 8y agoLooking at the design, it's clear that the team is trying really hard to make functions look and work like classes look. Variable declarations at the top, followed by helper methods, followed by lifecycle methods. Given that classes are themselves sugar to make prototypes palatable to a broad range of developers, it's certainly a tough design task; I'm not convinced that reliance on call-index is wrong per se, but it is magical (and magic in JavaScript is obnoxious). That said, the presentation of the third and forth flaws seem a bit weak. useState accepts a key as the first parameter, but in both of the composite functions that input parameter disappears. That looks like a refactoring error. From a design standpoint, if `useState(symbol)` is acceptable (and I'm not saying it is), then `useWindowWith(symbol)` would likewise be acceptable in that it's not adding any more requirements to the interface than the vanilla version. Proper bookkeeping of symbols resolves the issues here. However, if the design objective is to reduce the effort on the developer to do their own bookkeeping, then call-order indexing does make sense. (Not to mention that Symbols are not supported by IE11 which remains an important target for bigger companies.) I'm not sure there's a good way out. When you look at solutions like Protocol Buffers, they just bite the bullet and require the developer to supply the indexing. If JavaScript had something like Go's iota, then you could imagine using an enumeration to supply the indexing without requiring everyone to type 1,2,3,4... etc. But it doesn't so, that's wishful thinking. Nevertheless, the react codebase itself does contain a giant list of assignments of Symbol() || number to constants, so it's a pattern you're already aware of. A tough nut to crack.
- danabramov 8y ago>From a design standpoint, if `useState(symbol)` is acceptable (and I'm not saying it is), then `useWindowWith(symbol)` would likewise be acceptable in that it's not adding any more requirements to the interface than the vanilla version. I think you're missing that `useWindowWidth()` could have more than one `useState()` and thus you'd need to somehow compose Symbols. Which is what the next section is about.
- danabramov 8y agoIn particular, I would suggest to take the `useSubscription` example from "diamond problem" section and try to convert that whole snippet. You'll see where it falls apart.
- politician 8y agoI must be missing something because this didn't fall apart when I refactored it to work with a `useState(symbol, value)` interface. I'm not saying this is a good or ideal solution, indeed if the useWindowWidth hooks were to both useState and also useSubscription, then multiple keys would have to be injected -- it would work correctly, but the interface would have the AMD feel that perhaps is undesirable (per flaw #8). https://gist.github.com/politician/5f03c169a4119a63abb785b1c1249656 https://gist.github.com/politician/5f03c169a4119a63abb785b1c... EDIT: I still think it's a hard design problem and the concerns in flaw #8 are _very real_ to a broad range of developers. When you're just trying to get it done, proper bookkeeping of Symbols and injecting them into hooks composed of other hooks might cross the line. It's a shame that relying on call-order indexing is the solution because it's magical. But at the end of the day, engineering is about trade-offs. Time will tell whether deeply nested composition of automatically-managed hooks was a good feature to expose.
- WorldMaker 8y ago> It's a shame that relying on call-order indexing is the solution because it's magical. I've been thinking about this a lot since Hooks were introduced, and I'm increasingly of the opinion that it isn't that magical. Order of operations is incredibly important in the functions we write, especially in a language like JS that makes no effort for strict "pure" side-effect free functions. The order of a console.log or a return versus an increment matters in JS. We write a lot of procedural code in JS where order matters (a lot) already. In that matter, Hooks can just melt into the "procedural" background of JS. That said, I still feel like I want a better solution than "lint errors" for things like accidental branches of a Hook. I don't have any more of a proposal for how that would work or what that would mean than the article here, though, unfortunately.
- kmarc 8y agoI just briefly looked at the examples (and of course followed the twitter boom about it), but what I am not getting is that to me it looks like since useState() is against the principles of pure functional programming. I understand that it implements some kind of inversion-of-control, but just looking at the code it feels like using some global object's method which is a big no-no. Also this importance of ordering reminds me the unmaintainable magic hell of Angular. Maybe I'm just missing the "explicit-over-implicit" concept here. What's your opinion on this?
- danabramov 8y agoI suggest you to read about algebraic effects. We don't have them in JavaScript, but conceptually that's what Hooks represent. (This is as functional as it gets.) React has always been about taking useful ideas from functional programming and bringing it to mainstream through pragmatic choices in JS. Your concern about the "globalness" is addressed in Sebastian's comment which is linked five times throughout the post — you should definitely check it out! https://github.com/reactjs/rfcs/pull/68#issuecomment-439314884 https://github.com/reactjs/rfcs/pull/68#issuecomment-4393148... Finally, don't forget you're comparing Hooks to classes. Those are hardly functional either.
- asark 8y agoIt's not a global object—it's an instance of the object for the component you're using. The whole thing's a terrible OOP system in a language that already has a built-in mediocre OOP system, which terrible OOP system is, in the end, just a(n inefficient) pass-through to same built-in mediocre OOP system. The posters here wondering why you can't name them are on to something. I assume it's because then it'd be too obvious that's what they're doing, and whoever's paying people on that team (I really hope it's not more than one person, it's not hard work, but it probably is) might notice and make them stop, and maybe the React team at FB would even shrink in size, and we can't have that. I'm not sure what other explanation there could be for such comically-wasteful sandcastle building. I assume it's a combination of individual incentives to work on something not-difficult but flashy and prominent, with project incentives to never need fewer people than they currently have.
- drekembemutombo 8y agoEnjoying your posts, keep them up! Hooks look like a very powerful API for sharing pieces of functionality between components. You've probably heard this one a lot, but what bothers me a bit about hooks (especially useState) is that before, when you saw a component defined as a function, you could assume that it was just a function that renders something based on its props. However, I think it's a fair trade-off for such a powerful API.
- WorldMaker 8y agoRecent versions of React have encouraged truly pure props-only functions to wrap them with React.memo(), which also adds the benefit of giving them the equivalent of a shallow-check `shouldComponentUpdate()` avoiding re-renders when the props are the same. React.memo() is all the more useful of a signal in the Hooks world.
- brabara 8y agoThe first two comment snippets read like someone who woke up with a horse's head in their bed after refusing the proposal from the React Family.
- city41 8y agoI’m surprised Reagent’s ratoms weren’t an inspiration. They give you a lot of Hooks functionality largely through Clojure’s native atom.
- lilactown 8y agoYep. We've been using what is akin to an apollo-client hook at work for the past ~9 months or so: (defn my-component [] (let [user-data (data/pull! :user "{ user { firstName } }")] (fn [] (if (:loading @user-data) [:div "Loading..."] [:div "Hello, " (get-in @user-data [:data :user firstName]) "!"]))) I'm a bit surprised this pattern isn't more popular; having side-effectful functions return ratoms in a form-2 component seems almost as flexible as Hooks. It seems that most CLJS people try and keep their views more pure, which I think is honestly to their overall detriment. You miss out on the encapsulation and composition that React is espousing. That being said, I intend to replace reagent soon with just raw React + a few helpers :P
- lauritzsh 8y ago> That being said, I intend to replace reagent soon with just raw React + a few helpers Do you intend to keep using ClojureScript or use JavaScript/TypeScript? I read pure React with ClojureScript is rather painful (mostly due to the props conversion but maybe that's what your helpers are for?) I am asking since I am still considering between React (JS/TS) and Reagent/Rum (CLJS) for a side project of mine.
- deleted 8y ago[deleted]
- dmitriid 8y agoKeyword: clojure's native ratoms. You'd need to come up with a native ratom for Javascript somehow
- fogetti 8y agoBeg to differ, yes, implicit call order will result in huge clusterfucks. React+Redux is already causing Frankenstein apps (which is not implicitly caused by those frameworks (ok, maybe except redux) but when you throw in react-redux, react-router, redux-thunk, etc. in the mix it just deteriorates quickly). Well the NPM report showed us the trends. Every major fad peaks around 5 years in the making in JS land and then it fades away. We are halfway through. We just have to sit through the next 3-5 years.
- blindwatchmaker 8y agoWhat way of structuring frontend prior to the react/redux 'fad' do you think was superior?
- fouc 8y agoThat's a loaded question. What about comparing to the current day alternatives and upcoming ones? People have learned from react/redux and are pursuing alternatives.
- murukesh_s 8y agoDont think anything was superior but that doesnt mean its perfect. It still amaze me how much time we have to spent(engineer) to get a frontend working. It really should be a completely or semi completely visual process.
- woah 8y agoIt's always strange when people act indignant about the fact that engineering a large frontend is difficult, like the world owes it to them for it to be easy. Frontend code is by nature highly repetitive, but each different unit has its own arbitrary tweaks because of the fact that people have to use it. This makes it very hard to abstract. There are a million visual frontend builders out there, and they are all terrible, because you can never get them to do what you want. They are only usable to build example apps, and if used for anything real, require a huge amount of hacking around which results in code that is worse than what it would have been otherwise.
- deleted 8y ago[deleted]
- maaaats 8y ago> Flaw 2: One common suggestion is to let useState() accept a key argument (e.g. a string) that uniquely identifies a particular state variable within a component. I tried asking about that earlier here on HN[0], nice to finally at least get an explanation. The tldr is name clashes when reusing hooks. A valid concern, I think they should be more upfront about the reasoning instead of "hooks are magic, don't use them in these ways". That would make it easier to accept. [0]: https://news.ycombinator.com/item?id=18640612 https://news.ycombinator.com/item?id=18640612 (and the blogpost in the answer didn't really answer it)
- k__ 8y agoWouldn't symbols solve this issue?
- genezeta 8y agoYes, and to some extent it is addressed in the link too (though not really well explained). Symbols do not clash, but they need to be managed/stored by client code. i.e. you need to keep the original Symbol to use it in different calls, while you can use "different equal strings". This is the argument in the article. Whether this is indeed more or less desirable than having order restrictions on calls, that's a different thing. I personally think it is indeed a better solution, but the React team seems to think it's not.
- k__ 8y agoI guess changing call orders is an edge-case, so why optimize for it.
- danabramov 8y agoSymbols don't solve this without extra closure wrapper and Hook "instantiation" (described in flaw #5). Passing a Symbol to custom Hook from outside also doesn't work because a custom Hook may have more than one state. Try to convert the `useSubscription` example to your proposed API (and don't forget effects would also need "IDs") and you'll see what I mean.
- k__ 8y agoIsn't the ID problem just a question of the type of the ID? Sure strings would have collisions, but symbols wouldn't.
- danabramov 8y agoYou can search the article for "symbol" — there's a whole section dedicated to that. :-)
- k__ 8y agolol, that's what I did, somehow I didn't get any results. Maybe a typo, thanks for the heads up :)
- thomasfoster96 8y agoI think this post has actually pushed me back towards Symbol keys perhaps being a good idea. The useFormInput() example under Flaw 3 seems rather contrived – wouldn’t you just pass a Symbol key to useFormInput and it would then pass the key to useState, solving the supposed flaw? If you had to use useState several times in useFormInput, just use a WeakMap (they’re not that scary) with the keys being the Symbols passed to useFormInput. Or am I missing something in the explanation?
- genezeta 8y agoIt's not really well explained in the article, but the argument against Symbols is that you (client code) have to store them somewhere for reuse in different calls. That is that, you could do... useState("someID") ...and somewhere else (* or in the same place but on a different call) again... useState("someID") ...and this indeed refers to the same item. But using a Symbol, you need to first create it, store it somewhere and then use it. That is, you can't do this... useState(Symbol("someID")) ...because this will fail through different repeated calls. Instead you'd need to first... let someSymbol = Symbol("someID"); ...and then... useState(someSymbol) Or, alternatively, use Symbol.for("someID"), which then has both problems: creating the symbol first and clashing of identifiers. While the article does not explain this clearly, the example used alludes to this in an indirect way. Personally I do think that this would be a more desirable sacrifice to make than restricting call order, but the React team thinks otherwise, it seems.
- thomasfoster96 8y agoThat’s not really all that different to everyone moving their CSS-in-JS and GraphQL queries out of a functional component body though, is it? This is kind of exactly the scenario Symbols seem to have been intended for. Also... a nitpick, but `new Symbol` always throws a TypeError. And Symbol.for() is a useful escape hatch.
- genezeta 8y ago> Also... a nitpick, but `new Symbol` always throws a TypeError. And Symbol.for() is a useful escape hatch. Yes, you're right. I was distracted with other stuff and meant just Symbol, without new. I'll fix it. Thanks. As for the rest... Well, I don't really care much for React and many of the decisions they make. I don't like CSS-in-JS at all, and GraphQL... well, that one's nothing new.
- yiransheng 8y agoHooks reminds me of tensorflow scopes a little bit, in python: with tf.variable_scope("foo"): with tf.variable_scope("bar"): v = tf.get_variable("v", [1]) assert v.name == "foo/bar/v:0" `v` tensor here will have a generated human readable unique name here: "foo/bar/v:0" It seems with hooks, react uses call order to derive the unique "leaf" hook id for runtime resolving its implementations. However, it would be nice if react hooks can automatically provide a similar human readable "hook id" (even if only for dev/debug build). function useWindowWidth() { const [[width, setWidth], stateId] = debug(useState); assert(stateId === "useWindowWidth/state/:0"); useEffect(() => { ... }); const [_, effId] = debug(useEffect, () => { ... }); assert(effId === 'useWindowWidth/effect/:1"); return width; } This will definitely help for nested custom hooks..
- danabramov 8y agoFor debugging, we will show Hook tree in DevTools by capturing and parsing stack traces. https://github.com/facebook/react/pull/14085 https://github.com/facebook/react/pull/14085
- Klathmon 8y agoCould something like this be used to move the "linting" that the react team recommends directly into react itself? If you can abuse error stack traces to get the call stack, and some trickery to get the string representation of the component function that called the hook (following it through all intermediate custom hooks), you could then have the full text of the function body and know for sure that it's calling a Hook, and from there could run some linting on that internally and scream to the console if a hook is being used incorrectly. It may have a pretty significant performance and possibly size overhead depending on how much code is needed to inspect the function body and actually do the parsing/linting, but removing the need for a linter ("need" might be too strong of a word?) would make it easier to get started with, and safer to use for developers who, like it or not, don't read docs fully, don't setup or use linters, or just want to throw something together with very little tooling. And obviously it would all be stripped from production builds. Has the react team explored this idea? and are there reasons that I'm missing that it won't work or isn't ideal?
- drabinowitz 8y agoI wonder if it would makes sense to enforce hooks being called through a top level React function. To make some of the ordering more explicit function MyReactComponent() { const [ [width, setWidth], [name, setName], ] = React.use( [useWidth], [useName, 'alice'], ); return <div>{width} {name}</div> } admittedly, this is much less clean looking than the current proposal, but, in my mind at least, it makes it a bit more clear that you have to pass the functions in with a specific order at the top of the component.
- danabramov 8y agoI don't understand what exactly you're trying to solve by this (nothing prevents a user from making it conditional) but it definitely has flaw #7 (can't pass values between Hooks).
- drabinowitz 8y agoAh that's fair about flaw #7
- benmmurphy 8y agorelying on call order does seem kind of scary but without knowing how people structure their code using hooks its hard to know how bad it would be. for example the situation with hooks is basically: 1) any function that calls a hook function has a red colour 2) any function that calls a red coloured function has a red colour 3) if you ever have ever call a red coloured function in a conditional branch then bad things are going to happen if you have nested functions calling hooks then you can change the order of hooks without even realising hooks are being called which is dangerous. this 'nesting transparency' where callers aren't forced to know about the hook behaviour of their sub-functions is also used as defence in the blog for relying on call order. heh
- danabramov 8y agoThe "color" you're talking about is the "use" naming convention that's enforced by the linter. So if you call a Hook, you're supposed to call your function `useSomething()`, and we consider it a Hook too. In practice we haven't seen this to cause confusion from people who actually tried this proposal for more than a few hours. See https://reactjs.org/docs/hooks-rules.html https://reactjs.org/docs/hooks-rules.html
- sfvisser 8y agoSticking to attributes on classes doesn't have this ordering issue, because construction only happens once. class Form extends ReactishComponent { name = this.useState('Mary') surname = this.useState('Poppins'); width = this.useState(window.innerWidth); constructor () { this.useEffect(() => { const handleResize = () => this.width.set(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }) } handleNameChange = e => this.name.set(e.target.value) handleSurnameChange = e => this.surname.set(e.target.value) render () { return ( <> <input value={this.name.get()} onChange={this.handleNameChange} /> <input value={this.surname.get()} onChange={this.handleSurnameChange} /> <p>Hello, {this.name.get()} {this.surname.get()}</p> <p>Window width: {this.width.get()}</p> </> ) } }
- danabramov 8y agoHow do custom Hooks look in this world?
- linkmotif 8y agoDan, thanks for your work on React and all the new features from you and the team. I love your stewardship of this project. React is fantastic and I’ve loved your choices of features to add. If you could just make docs less verbose...
- danabramov 8y agoThen other people will ask to make them more detailed :-) Thanks for feedback though, we’re listening.
- deleted 8y ago[deleted]
- 8y ago
- Tade0 8y agoMy gut feeling tells me that introducing hooks is going to end badly. If there's no patently obvious advantage but you have to rely either on convention or an additional pool of knowledge then most junior (or generally less skilled) developers cannot be trusted to use the given thing properly. I've seen this happen with observables - sure you can do a lot of new stuff with them but they are only clearly more useful than say promises in a handful of cases. Thid trend of producing tools which are powerful in the hands of the best but hard to use for beginners worries me. In the long run this makes development more expensive, not less.
- nihakue 8y agoRe: 'Flaw #6: We Still Need a Linter' (first example): Could the 'primitive' hooks (useState, useEffect, etc) walk up `arguments.callee.caller.arguments.callee.caller...` grabbing function names until you hit a React function? Then use the names to create a 'composed' key automatically? It still doesn't solve the problem of a function using the same hook twice in one function, but it might solve the problem of collision across custom hooks. Example: function useCount() { const [count, setState] = useState(0) return { count, increment: () => setState(count + 1)}; } function useCountPlusOne() { const {count: baseCount, increment} = useCount() return {count: baseCount + 1, increment} } function MyHookComponent() { const { count, increment } = useCountPlusOne() return ... } Would give you a key of `useState(useCount(useCountPlusOne(MyHookComponent)))` without the end user having to futz around composing the key manually. At this point you could probably even forego the 'use*' convention It's still pretty magical, but the magic seems more abstracted. In general I've really liked hooks, and I'm willing to put up with the wackiness (although testing them with enzyme is a big PITA right now). Thanks for the article :)
- WorldMaker 8y agoThere are some big performance implications of using `arguments` (most current JITs heavily deoptimize functions that use `arguments`), and arrow functions in the spec are supposed to throw errors for any attempts to access their `arguments`. It's probably not a good idea for Production code.