Realigning Your Model of React After Hooks - With Dan Abramov
Dan Abramov discusses how the addition of hooks changed how we write React.
In this episode Kent and Dan talk about the ways in which you'll have to reconstruct your mental model of how React works in order to get the hang of hooks, and how hooks more closely align with React's intended model.
React has made multiple attempts at figuring out a way to share state between components. Mixins, higher-order components, render props, and now hooks. Dan Abramov walks through what went right and what went wrong with each of the implementations prior to hooks. None of these implementations lined up with React's model, they were too indirect, or too limiting.
Classes didn't properly fit-in with React's component model either. Components don't use inheritance, they aren't ever instantiated, you don't call methods off of them. Dan explains how Components are more like a stateful function, and how Hooks are a closer aproximation of this mental model.
Kent C. Dodds: All right. Hey, everyone. This is your friend, Kent C. Dodds, and I am with my friend, and hopefully your friend too, Dan Abramov. Say hi, Dan.
Dan Abramov: Hey, there.
Kent C. Dodds: You're so enthusiastic, I love it. Hey, there. All right, so Dan, I am super excited to talk with you always, like any time. I really enjoy our conversations. Yeah, today I wanted to talk about ... Well, maybe it'd be good to give an introduction to you. I kind of take for granted that people will know who you are, but it'd be great if you could give us a little intro to yourself. Tell us who you are, what you're about, what you're doing, and maybe you could just say one non-tech thing that you're interested in?
Dan Abramov: Okay. Right, so my name is Dan Abramov. I work on the React Team on Facebook. React is a UI library, but if you're watching this, you probably already know that. One non-tech thing that I'm interested in? I haven't had any hobbies for a while. I think the only non-tech I'm doing lately is watching Netflix. I don't know if that counts, but ...
Kent C. Dodds: I've been there, for sure. Sometimes you're so into it.
Dan Abramov: I play Fortnite, but really badly.
Kent C. Dodds: Nice. Very, very cool. Cool. Today, Dan, I wanted to talk with you about Hooks, of course. That's kind of the thing that is on a lot of people's minds, whether they're into React or not. We have the people who are super interested in this new Hooks thing almost to a fault, and then I think maybe I lump myself in that category a little bit, but then we have people who are really skeptical of it, and like, "Oh, I guess you just must hate classes, and you want to make things complicated just to make things complicated. Whatever." So there's a lot to unpack here with Hooks. And I think there are a lot of really good resources for people getting started with Hooks, so I don't want to go too far into that. You know, Reactjs.org/hooks, and watch that video, and you'll get a really good intro the concept of Hooks. But yeah, why don't we kind of unpack the idea of ... Or maybe we could start with just a little brief why-Hooks thing, and then we can get into some more details there.
Dan Abramov: All right. Do we assume that people watching this already watched the video introduction, or do you want me to recap it?
Kent C. Dodds: You can give us a 20-30 second little intro to conceptually what Hooks is.
Dan Abramov: I think the way I think about Hooks is really two parts. One part is that we ... Hooks lets you reuse behavior between components more easily, in a very flexible way. Another part is, so in order to do that, we need to give you some [primitives 00:03:13] that you can ... Like, the building blocks that let you do that. The second part is that those building blocks turn out to be sufficient to implement pretty much all React features. In that sense, Hooks also lets you write code without declaring the component class, because it turns out that those building blocks that you used to build these reusable behaviors, they're also sufficient for just declaring components. You can think of Hooks as a new component API that is a little bit more flexible.
Dan Abramov: I think it's kind of hard to explain, because all of those things kind of tie together. Every one of those patterns, I'm not saying that they're bad. Like even mixins are kind of okay-ish, but eventually you just run into walls with them. With mixins specifically, they suffer from the same problems as multiple inheritance, because they technically are multiple inheritance. Eventually, as you use them more and more, you're going to run into [naming 00:05:56] clashes. You might have two mixins, but you can't use them together, because maybe they both use some other mixin, but they want different things from it. They don't really compose well. The higher order component pattern, it solves the composition problem, so you don't get name clashes [in 00:06:23] methods, but then the problem ... So higher order components have a few problems. One of them is just the code becomes very indirect. I think that a big ... Like, we consider it kind of like a principle, or a value I guess for React, is that we prefer direct code. I guess we don't discourage it, but I think we prefer when it's possible to write code that you can read it, and you can understand what's going on without doing these mental gymnastics of like, this thing, it actually gets into this [inaudible 00:07:05] function, and then ... We want to see the connections between things. With higher order components ... Yeah, go ahead.
Kent C. Dodds: Oh, I was just going to add, that also is I think what makes higher order components really difficult to type with, [Flow type 00:07:23], or [TypeScript 00:07:24], is that layer of indirection, for sure.
Dan Abramov: Yeah, so that's another related problem, but even if just look at some prop, and they're like, "Where is this prop coming from?" Well, if it's just the regular React component, it comes from a private component. But maybe there's like five higher order components in the middle, and each of them passes something to the other one, and you don't see it unless you actually go into them. The whole dataflow becomes obscured, so that's one of the problems with higher order components. They also can have name clashes, not in the method names, but in prop names. That's also an issue, and method calls, you need to forward ... There are some [inaudible 00:08:09] cases, but anyway, render props solve some of those. With the render props, they seem to be closest to React. They don't suffer from a lot of those problems, because they kind of embracing rendering. React's components' life happens in the render method. That's really the [Reacty 00:08:35] part of it. The rest is just, "Yeah, fire this sometimes, fire this sometimes." But really, the big picture happens in the render method. And so the render prop's pattern is ... By putting more stuff into render, it benefits from this explicit dataflow, and explicit composition, and you can trace every value exactly where it's coming from. If you want something a few layers deeper, and then you have props a few layers above, you can just reference it, because it's in the [closure 00:09:06]. I think the render props are really helpful in that sense, but if you ever tried to use render props more widely than a few components, you're going to have this ... Just like callback [hells 00:09:23], or we call this [inaudible 00:09:24] hell, where there is a notion of tree structure, so you can have this locate, meta-query, location, watcher, and fetch, or something like that. So you have a pyramid of them, but conceptually, there is no clear hierarchy between them. So when we talk about the hierarchy of React components, we use it because they pass some values to each other, because they may have some local state, they may want to show its children, hide in its children, and stuff like this. But with a tree of render props, conceptually they're just a list. It's just a list of things you care about, and then you use them in rendering. [Dominick 00:10:13] had this idea about a custom syntax what would look kind of like [async/await 00:10:20], but it would instead [inaudible 00:10:24] these trees of render props into ... Like [inaudible 00:10:29] so that you can write them as plain function calls, and I think based on that, Sebastian came up with this idea of Hooks, which it's not using render props, but conceptually, it's like you use things in render by just calling functions, but those can have state and side effects encoded in them. So this allows to express the same patterns as render props, like 90% cases, where you don't actually want this nesting, but they're just as expressive. What's cool about it is Hooks are a bit more expressive than just render props. As an example, if you used render props, you've probably run into this a lot of times. Maybe you have a render prop, and it gives you some value. Then you're like, "Oh, actually, I need to use this value in the lifecycle method."
Kent C. Dodds: Yes, absolutely.
Dan Abramov: Good luck, you can't do that. You have to split the component in two.
Kent C. Dodds: No, you can't do it. Yep. Mm-hmm (affirmative).
Dan Abramov: Right. So with Hooks, you don't have this problem, because there is no false hierarchy. The whole thing happens in render, so you just get that value, it's in render, and then instead of lifecycles, and you have effects, which can reference values and renders. So it is just naturally there. There is no special API to read context in the lifecycle, or read some render pop in the lifecycle. Things can just refer to each other. I think that's the real benefit that I saw that sold me on Hooks is just that they express all of these patterns in a very straightforward way.
Kent C. Dodds: Yeah, I love it. I think we're always in ... Probably coding in general, we're always trying to find better ways to share logic in a way that doesn't leave us with code that's hard to maintain. I feel like every step of the way, we got a little bit closer to that. It's cool to think, or to realize that Hooks kind of came from this idea of that special syntax for adopting ... It was adopt keyword, right?
Dan Abramov: Yeah.
Dan Abramov: I think the biggest one that I'm seeing is just ... Like, especially if you already come from ... Like, if you already used [inaudible 00:14:36] some React, or you used another library that kind of looks like React, you might have an existing mental model of how lifecycles work, when is render called. I think that if you come with that perspective, when I look at code using Hooks, you might have two kinds of problems. One kind of problem is when you just don't get what's going on, you're just confused. But another more subtle problem is when you just apply your mental model, like from classes, and kind of make a one-to-one mapping to each concept. Like use effect is just lifecycle, or this thing is just that thing. Then when this mental model doesn't quite match, because conceptually it is a little bit different. I think if you don't internalize this difference, and like if you don't realize that you need to think in Hooks, you need to unlearn what you knew before, and try to think about it in a different way, I think that can be challenging. However, I am not too worried, because looking at the beginners, and people who don't have experience with class React, I find that they catch on to using Hooks way faster. It's mostly the existing habits, and existing mental models that can be hard to break. But another thing that comes up is that people say, "Oh, this is not intuitive. Class lifecycles were way more intuitive." I can see where they're coming from, and in some cases, I agree that it might be more obvious how to use a specific class lifecycle in this specific situation, but also I've been here for a while. I've been on React [inaudible 00:16:46] since 2014, and I remember people getting wildly confused about the class lifecycle methods, and they didn't know where to put the data fetch, and should they [ever 00:17:02] do it on [componentDidMount 00:17:03], or [componentDidUpdate 00:17:04], [componentWillMount 00:17:06]. There's so many methods, which one do you pick? I think it took a few years before it kind of ... And like rendering also, people would create dominoes in render. People would be super annoyed that, "Well, why can't I just [jQuery addClass 00:17:23]? Why do I have to setState in order to toggle a class? This is ridiculous." I think it took a few years for this paradigm of UI driven by props and states to kind of propagate into the mainstream, and be adopted by other libraries and frameworks. The idea of top-down dataflow, and pure or [inaudible 00:17:54] render method, and I think that's ... Like, what I see around Hooks is, it looks a lot like Reaction 2014, where nobody really knew how to do ... Like, I'm still figuring out how to do some tasks, because it's just a different mental model, and sometimes I look at the code, and I'm like, "Wait, I'm not sure how to translate it." And then you just spend some time, and then you're like, "Oh, okay. This makes sense." The next time you see this problem, I'm like, "Yeah, we solved this before." And this is the exact process I went through in 2014 with class lifecycles. But I guess the takeaway for me here is that if you're not comfortable figuring things out, then it might be a bit of an early time to adopt Hooks, because there are no best practices yet. This is a bit of a wild west, because we're all experimenting. And like at Facebook, we're still figuring out how to solve certain things, and it'll take some time, but I think the paradigm itself is solid, and it's just that we're still kind of figuring out the consequences of the paradigm.
Kent C. Dodds: Mm-hmm (affirmative). I like what you've said really well. I think it seems to me like more ... It's less what's intuitive, and more we were used to doing things a certain way, and now somebody moved my cheese. Now things are different, and I'm trying to map what I did before to this new thing. I used to just use show and hide with jQuery. Now I have to do the set thing, and so therefore it is not intuitive.
Dan Abramov: Yeah, and lifecycles in effect, it's like a direct analogy. You used to do [addClass 00:19:45] whenever you want, and now you have to setState, and that confused lot of people back then. But now, [inaudible 00:19:53], like I used to do fetch in componentAddMount whenever I want, and now I have to have an effect with dependencies that is driven by state. Wait. Why don't I just fire [it 00:20:07] off on mount? Why don't we have like [useOnMount 00:20:09]? Or like [useOnUpdate 00:20:09]? This is a very similar problem, because what React did in 2014 was it introduced this top-down dataflow into rendering, and so with Hooks, the same concept gets introduced into effects.
Kent C. Dodds: Mm-hmm (affirmative).
Dan Abramov: But people are not yet used to thinking of dataflow, and how that influences effects. They're used to doing one-off operations that are ... And so the problem with those is that, just like with addClass, your rendering output would often be inconsistent, because it's just like [inaudible 00:20:47] accumulates over time. If you forget to do it in some cases, your CSS class is going to be wrong, and it is going to look wrong. So the same problem happens in class lifecycles, where you might do fetch in componentDidMount, but a lot of people don't know that you're supposed to also do it in componentDidUpdate. But then you also need to handle [race 00:21:06] conditions, and make sure that if requests arrive out of order, then it still works. So there are a lot of gotchas with how you do it in classes, but many people just don't think about, and then they have bugs and then they figure it out. And with effects, they force you to confront that ... Hey, there is top-down dataflow, and here it's like, what do you want to do? And how does that thing feed into this thing? Sometimes you have to write a bit more code, and some patterns that are inconsistent, they will just like ... In React rendering, if you want to render different things on mount and updates, it's more code than if you always render the same thing depending on the state. It takes some adjustment, but the goal is to make rendering more predictable, and to make the side effects from the component more predictable.
Kent C. Dodds: Yeah, I love that. You know, one thing that I had to change about my mental model was on click handlers. Typically, that's when I would go do the fetch call, or that's where I'd set it in local storage, whatever the case may be. I've found that with Hooks, it actually makes more sense to just call a state updater method, and setState for this thing, and have it re-render, and have an effect do that. That has the really nice side effect, no pun intended, of making it so I don't forget to run that side effect when that state would change in other places.
Dan Abramov: Yeah.
Kent C. Dodds: In the jQuery days, you'd say, "Oh, I have to hide it here, but then there's this other case where I'm setting that state somewhere else, and I have to hide it here or over there." With React components, you've have something in componentDidMount and that'd be all the logic there, and then you realize, "Oh, I need to do that also if they click this button, so we're going to put that into another method, and we'll just call that in all these different places." But in reality, you pretty much always want those things to happen when certain elements of state changes, or whatever, and so you've put it into an effect, and it becomes a lot simpler in my mind to do things that way. So I feel like the cool thing about Hooks is not only does it give us this great way of sharing logic, which is probably the biggest thing that I'm excited about, but it also gives us a new way to think about interacting with the React APIs in a way that's a lot more expressive.
Dan Abramov: Yeah, I think it's just ... I mean, I don't disagree that in some cases, it can be more difficult. If you intentionally want a different behavior that is not consistent for some reason, between [inaudible 00:23:49] and updates, you have to write a bit more code. It kind of forces you to really kind of model the dataflow, which I think would even be worrying to me. That's something we've tried to avoid in React in general. We don't really want ... You know there are libraries where you have to learn 20 different API signatures in order to do even simple things? And you need to know how to compose them together, combine them together, and which one of them to use in which situation. That's a trade-off, it's like, you have it like this [inaudible 00:24:35] of different things, and you become a master at them, but this is not something that we generally want in React. We try to avoid this upfront cost of getting something right. So I think with effects, it could be a bit worrying that we are kind of forcing you to write correct code earlier, but you struggle doing that, whereas like you could've just wrote something that works half of the time, but at least it can [inaudible 00:25:10], and you can ship the next feature, and eventually you'll fix the bugs. So that could be a concern. The reason I'm not too concerned about it is custom Hooks. When you get it right, if useEffect was the only primitive available to you, and you had to use it directly everywhere, I think that would not be worth it. But the point is that once you get it right, you can actually put it into a custom hook, and then this custom hook has a simple API. For you, it could be just fetch, or use this thing from your application that used [inaudible 00:25:49] caller, and you don't really go there again unless it breaks. So the resilience, and the correctness afforded by getting it right when you write this useEffect, then it becomes this foundational stone upon which all of your custom Hooks are built, and then you know that those work consistently. It takes some effort to write them, but then you don't write a new one every day. You just reuse the ones that already exist. I think that's one of the reasons people are a bit confused right now, because we're just used to putting stuff into classes. So like similarly, whenever we want to do some lifecycle, even if we do that in all classes, like if [crosstalk 00:26:37] just copy-paste the same code, with Hooks, I think it's important to know you can extract things, and extract common patterns, and use them all across your app. It's totally expected that your app would have its own set of custom Hooks, and then eventually you wouldn't write useEffect every day. You would just reuse those ones.
Kent C. Dodds: Yeah, and that's one of the things I love about it. With classes, we talk about the separation of concerns, and you can have multiple concerns in a single component. You know, my concern is update the document title, my concern is subscribe to Firebase, but I'm putting them all in one component. And then I decide, you know, I want to separate these out, and the story for doing that between classes and hooks is wildly different, where with classes, I have to inspect six different methods, and lifecycle Hooks, and then put that in some sort of render prop thing. Where with Hooks, I just literally select all the text that I want, and if I'm removing it, I just delete it. If I'm moving it somewhere else, I just copy or cut and paste. That one element alone is just so valuable, and just lends itself to easier maintainability. We're kind of coming down on our time a little bit, Dan, but I want to ask you a question that maybe you can give us some insight into the React team a little bit. What's the deal? Do you just really hate classes? Is that why you went through all of this effort to just ... "Man, classes are the worst." Because classes, they seemed all right. We mentioned a couple problems, but I think I've seen a lot of people just say, "Oh, React folks just hate classes. That's why they went through all this trouble."
Kent C. Dodds: Kind of like React Future in the past?
Kent C. Dodds: Mm-hmm (affirmative).
Dan Abramov: It's just you need to kind of internalize this, that they're not going to change under you. But there's another reason which I think why this is not really a problem. It's that if you don't know that, and you use classes, you're going to run into a box caused by reading like this.props or this.state in some async callback, but those have already changed, because maybe you move to a different page. It's like if you follow a user, and then eventually you read this.prompts.user, but maybe you navigate to another page while we were doing that. So you don't want to follow the wrong user because of it. So the solution to that is a closure. Even if you use classes, you have to learn closures in order to solve this box.
Kent C. Dodds: Mm-hmm (affirmative).
Kent C. Dodds: Yeah. You actually tweeted out a demo, a code sandbox for that specific example, right?
Dan Abramov: Yeah, so I tweeted an example of ... I think tweeted the example of a class component, and asked how you would convert it to a function, and like ... Oh, no. I made an example of function component, and [inaudible 00:41:35] how would you convert to a class? And everybody did ... Out of like 30 or 40 replies, I think only two were correct, because they didn't add a bug into the clause. But most class examples were buggy, because people didn't realize that they needed a closure to fix it.
Kent C. Dodds: Mm-hmm (affirmative), yeah. Let's see if we can find that, so we include that in the [crosstalk 00:42:03]-
Dan Abramov: Well, I deleted the tweet, because-
Kent C. Dodds: Because you got too many replies?
Dan Abramov: Yeah, and I was just preparing a blog post. The blog post is called ... It's on my blog. Oh, I'm actually going to plug it here now.
Kent C. Dodds: No, yeah.
Kent C. Dodds: Cool. Well, Dan, this has been a super enlightening chat. I definitely want to talk with you a little bit more about the future of React, and where React is headed. We'll do that in another episode. Before we end this, is there anything in particular that you want to mention to our listeners?
Dan Abramov: I don't know. I hope you kind of like React, or not dislike it too much. I hope you're not forced to use it. If you are, I'm sorry. But also, yeah, I just wanted to mention that it's an early time for Hooks, and we're still figuring out how to do things with them. If you're kind of an early ... I don't know. If you're the kind of person like I was in 2014, who just wanted to play with something, wanted to figure out how it works, I wanted to figure out the patterns, I think I would appreciate if you tried Hooks, and if you tried to make something interesting with them. Give us feedback, raise an issue if something is not clear, we'll try to help you. But on the other hand, if you prefer when there are best practices, and when every question has an obvious answer, and when there are tutorials and articles that completely describe something, it might be better to wait out a little bit, and wait for those best practices to actually emerge. Don't rush into rewriting anything. Just give it some time to shake out ,unless you want to help. If you want to help then that would be cool.
Kent C. Dodds: Yeah, yeah. Absolutely. I think that's really great advice. It's fun to try these new things, but acknowledge the fact that when you're using something new, you're gonna make all your mistakes at the beginning. So I probably wouldn't rewrite your checkout button in your first try with Hooks.
Dan Abramov: Unless it has a bug caused by class, then sure.
Kent C. Dodds: True, true. Just make sure you're writing tests. Cool. All right, so that's it for today, or for this episode. Thanks so much, Dan. I really appreciate you taking the time to chat with us about Hooks. I think this will be helpful to people, so thank you.
Dan Abramov: Thank you.
Kent C. Dodds: All right, see you later, everybody!