Senin, 15 Juni 2020

Preparing for a Tech Talk, Part 2: What, Why, and How

I’ve done a few tech talks that I think went well.

Sometimes people ask me how I prepare for a talk. For every speaker, the answer is very personal. I’m just sharing what works for me.

This is the second post in a series where I explain my process preparing for a tech talk — from conceiving the idea to the actual day of the presentation:


In this post, I will focus on finding the What, Why, and How of my talk. Doing this early helps me avoid a lot of headache at a later stage.


If you haven’t seen Inception, watch it today. It’s an enjoyable blockbuster with mindbending visuals and a stimulating plot. But as Ryan Florence taught me, it also contains good advice for creating a memorable talk.

That movie is about putting ideas into other people’s heads while they sleep. This might sound a bit invasive (and is illegal in the movie). But if you signed up to give a tech talk, that’s a pretty accurate description of your challenge.


What is the one thing that you want people to take away from your talk? I try to formulate it as a sentence early on. This idea shouldn’t be longer than a dozen words. People will forget most of what you say so you need to pick carefully what you want to stick. It’s the seed you want to plant in their heads.

Spinning top from the Inception movie

For example, here’s the core ideas of my talks.

I don’t always explicitly say the central idea out loud or write it on a slide, but it is always the intellectual backbone of my talk. Everything I say and show must ultimately work towards supporting this idea. I want to prove it to you.


An idea is the “What” of my talk. But there is also “How” and “Why”:

Pyramid: “How” is on top of “What”. “What” is on top of “Why”.

“How” is my method for delivering the idea to the audience. Personally, I prefer live demos, but there are many things that can work. I will talk more about “How” in the later blog posts in this series.

We’ve just discussed “What” which is the core idea of the talk. It’s the thought I want to plant in your head and the insight I want you to walk away with. It’s what I want people to share with their friends and colleagues.

Which brings us to “Why”.


To explain “Why”, I’ll quote this dialog from the Inception movie:

(warning: spoilers!)

Cobb: "I will split up my father's empire." Now, this is obviously an idea that Robert himself will choose to reject. Which is why we need to plant it deep in his subconscious. The subconscious is motivated by emotion, right? Not reason. We need to find a way to translate this into an emotional concept.

Arthur: How do you translate a business strategy into an emotion?

Cobb: That's what we're here to figure out, right. Now Robert's relationship with his father is stressed, to say the least.

Eames: Well can we run with that? We could suggest him breaking up his fathers company as a "screw you" to the old man.

Cobb: No, cause I think positive emotion trumps negative emotion every time. We all yearn for reconciliation, for catharsis. We need Robert Fischer to have a positive emotional reaction to all this.

Eames: Alright, well, try this? "My father accepts that I want to create for myself, not follow in his footsteps."

Cobb: That might work.

Now, I’m not suggesting that you break up empires with your talk.

But there must be a reason you get out in front of thousands of people to speak about something. You believe in something — and you want others to share that feeling. This is the “Why” — the emotional core of your talk.


Here’s the example “What”, “Why”, and “How” from my talks.

How: “Live demo”. What: “Functional principles improve the developer experience”. Why: “Create your own tools to make programming fun”.

(The above pyramid is for Hot reloading with time travel)

How: “Live demo”. What: “Waiting for CPU and IO has a unified solution”. Why: “React cares about both user and developer experience”.

(The above pyramid is for Beyond React 16)

How: “Live demo”. What: “Hooks make stateful logic reusable. Why: “Hooks reveal the true nature of React”.

(The above pyramid is for Introducing Hooks)

A memorable talk takes a concise idea, makes the audience care about it, and has a clear and convincing execution. That’s the “What”, “Why”, and “How”.


In this post, I described how I organize the core ideas of my talks. Again, I want to emphasize I’m just sharing what works for me — there are many kinds of talks and your outlook on this may be very different.

In the next posts in this series, I will talk about preparing the talk outline, slides, rehearsing the talks, and what I do on the day of the presentation.

Next in this series: Preparing for a Tech Talk, Part 3: Content.

Previous in this series: Preparing for a Tech Talk, Part 1: Motivation.

The Elements of UI Engineering

In my previous post, I talked about admitting our knowledge gaps. You might conclude that I suggest settling for mediocrity. I don’t! This is a broad field.

I strongly believe that you can “begin anywhere” and don’t need to learn technologies in any particular order. But I also place great value in gaining expertise. Personally I’ve mostly been interested in creating user interfaces.

I’ve been mulling over what it is that I do know about and consider valuable. Sure, I’m familiar with a few technologies (e.g. JavaScript and React). But the more important lessons from experience are elusive. I never tried to put them into words. This is my first attempt to catalog and describe some of them.


There are plenty of “learning roadmaps” about technologies and libraries. Which library is going to be in vogue in 2019? What about 2020? Should you learn Vue or React? Angular? What about Redux or Rx? Do you need to learn Apollo? REST or GraphQL? It’s easy to get lost. What if the author is wrong?

My biggest learning breakthroughs weren’t about a particular technology. Rather, I learned the most when I struggled to solve a particular UI problem. Sometimes, I would later discover libraries or patterns that helped me. In other cases, I’d come up with my own solutions (both good and bad ones).

It’s this combination of understanding the problems, experimenting with the solutions, and applying different strategies that led to the most rewarding learning experiences in my life. This post focuses on just the problems.


If you worked a user interface, you’ve likely dealt with at least some of these challenges — either directly or using a library. In either case, I encourage you to create a tiny app with no libraries, and play with reproducing and solving these problems. There’s no one right solution to any of them. Learning comes from exploring the problem space and trying different possible tradeoffs.


  • Consistency. You click on a “Like” button and the text updates: “You and 3 other friends liked this post.” You click it again, and the text flips back. Sounds easy. But maybe a label like this exists in several places on the screen. Maybe there is some other visual indication (such as the button background) that needs to change. The list of “likers” that was previously fetched from the server and is visible on hover should now include your name. If you navigate to another screen and go back, the post shouldn’t “forget” it was liked. Even local consistency alone creates a set of challenges. But other users might also modify the data we display (e.g. by liking a post we’re viewing). How do we keep the same data in sync on different parts of the screen? How and when do we make the local data consistent with the server, and the other way around?

  • Responsiveness. People can only tolerate a lack of visual feedback to their actions for a limited time. For continuous actions like gestures and scroll, this limit is low. (Even skipping a single 16ms frame feels “janky”.) For discrete actions like clicks, there is research saying users perceive any < 100ms delays as equally fast. If an action takes longer, we need to show a visual indicator. But there are some counter-intuitive challenges. Indicators that cause the page layout to “jump” or that go through several loading “stages” can make the action feel longer than it was. Similarly, handling an interaction within 20ms at the cost of dropping an animation frame can feel slower than handling it within 30ms and no dropped frames. Brains aren’t benchmarks. How do we keep our apps responsive to different kinds of inputs?

  • Latency. Both computations and network access take time. Sometimes we can ignore the computational cost if it doesn’t hurt the responsiveness on our target devices (make sure to test your app on the low-end device spectrum). But handling network latency is unavoidable — it can take seconds! Our app can’t just freeze waiting for the data or code to load. This means any action that depends on new data, code, or assets is potentially asynchronous and needs to handle the “loading” case. But that can happen for almost every screen. How do we gracefully handle latency without displaying a “cascade” of spinners or empty “holes”? How do we avoid “jumpy” layout? And how do we change async dependencies without “rewiring” our code every time?

  • Navigation. We expect that the UI remains “stable” as we interact with it. Things shouldn’t disappear from right under our noses. Navigation, whether started within the app (e.g. clicking a link) or due to an external event (e.g. clicking the “back” button), should also respect this principle. For example, switching between /profile/likes and /profile/follows tabs on a profile screen shouldn’t clear a search input outside the tabbed view. Even navigating to another screen is like walking into a room. People expect to go back later and find things as they left them (with, perhaps, some new items). If you’re in the middle of a feed, click on a profile, and go back, it’s frustrating to lose your position in the feed — or wait for it to load again. How do we architect our app to handle arbitrary navigation without losing important context?

  • Staleness. We can make the “back” button navigation instant by introducing a local cache. In that cache, we can “remember” some data for quick access even if we could theoretically refetch it. But caching brings its own problems. Caches can get stale. If I change an avatar, it should update in the cache too. If I make a new post, it needs to appear in the cache immediately, or the cache needs to be invalidated. This can become difficult and error-prone. What if the posting fails? How long does the cache stay in memory? When we refetch the feed, do we “stitch” the newly fetched feed with the cached one, or throw the cache away? How is pagination or sorting represented in the cache?

  • Entropy. The second law of thermodynamics says something like “with time, things turn into a mess” (well, not exactly). This applies to user interfaces too. We can’t predict the exact user interactions and their order. At any point in time, our app may be in one of a mind-boggling number of possible states. We do our best to make the result predictable and limited by our design. We don’t want to look at a bug screenshot and wonder “how did that happen”. For N possible states, there are N×(N–1) possible transitions between them. For example, if a button can be in one of 5 different states (normal, active, hover, danger, disabled), the code updating the button must be correct for 5×4=20 possible transitions — or forbid some of them. How do we tame the combinatorial explosion of possible states and make visual output predictable?

  • Priority. Some things are more important than others. A dialog might need to appear physically “above” the button that spawned it and “break out” of its container’s clip boundaries. A newly scheduled task (e.g. responding to a click) might be more important than a long-running task that already started (e.g. rendering next posts below the screen fold). As our app grows, parts of its code written by different people and teams compete for limited resources like processor, network, screen estate, and the bundle size budget. Sometimes you can rank the contenders on a shared scale of “importance”, like the CSS z-index property. But it rarely ends well. Every developer is biased to think their code is important. And if everything is important, then nothing is! How do we get independent widgets to cooperate instead of fighting for resources?

  • Accessibility. Inaccessible websites are not a niche problem. For example, in UK disability affects 1 in 5 people. (Here’s a nice infographic.) I’ve felt this personally too. Though I’m only 26, I struggle to read websites with thin fonts and low contrast. I try to use the trackpad less often, and I dread the day I’ll have to navigate poorly implemented websites by keyboard. We need to make our apps not horrible to people with difficulties — and the good news is that there’s a lot of low-hanging fruit. It starts with education and tooling. But we also need to make it easy for product developers to do the right thing. What can we do to make accessibility a default rather than an afterthought?

  • Internationalization. Our app needs to work all over the world. Not only do people speak different languages, but we also need to support right-to-left layouts with the least amount of effort from product engineers. How do we support different languages without sacrificing latency and responsiveness?

  • Delivery. We need to get our application code to the user’s computer. What transport and format do we use? This might sound straightforward but there are many tradeoffs here. For example, native apps tend to load all code in advance at the cost of a huge app size. Web apps tend to have smaller initial payload at the cost of more latency during use. How do we choose at which point to introduce latency? How do we optimize our delivery based on the usage patterns? What kind of data would we need for an optimal solution?

  • Resilience. You might like bugs if you’re an entomologist, but you probably don’t enjoy seeing them in your programs. However, some of your bugs will inevitably get to production. What happens then? Some bugs cause wrong but well-defined behavior. For example, maybe your code displays incorrect visual output under some condition. But what if the rendering code crashes? Then we can’t meaningfully continue because the visual output would be inconsistent. A crash rendering a single post shouldn’t “bring down” an entire feed or get it into a semi-broken state that causes further crashes. How do we write code in a way that isolates rendering and fetching failures and keeps the rest of the app running? What does fault tolerance mean for user interfaces?

  • Abstraction. In a tiny app, we can hardcode a lot of special cases to account for the above problems. But apps tend to grow. We want to be able to reuse, fork, and join parts of our code, and work on it collectively. We want to define clear boundaries between the pieces familiar to different people, and avoid making often-changing logic too rigid. How do we create abstractions that hide implementation details of a particular UI part? How do we avoid re-introducing the same problems that we just solved as our app grows?


Of course, there are many problems I haven’t mentioned. This list is by no means exhaustive! For example, I haven’t talked about the designer and engineering collaboration, or debugging and testing. Maybe another time.

It’s tempting to read about these problems with a particular view library or a data fetching library in mind as a solution. But I encourage you to pretend that these libraries don’t exist, and read again from that perspective. How would you approach solving these issues? Give them a try on a tiny app! (I’d love to see your experiments on GitHub — feel free to tweet me in response.)

What’s interesting about these problems is that most of them show up at any scale. You can see them both in small widgets like a typeahead or a tooltip, and in huge apps like Twitter and Facebook.

Think of a non-trivial UI element from an app you enjoy using, and go through this list of problems. Can you describe some of the tradeoffs chosen by its developers? Try to recreate a similar behavior from scratch!

I learned a lot about UI engineering by experimenting with these problems in small apps without using libraries. I recommend the same to anyone who wants to gain a deeper appreciation for the tradeoffs in UI engineering.

Things I Don’t Know as of 2018

People often assume that I know far more than I actually do. That’s not a bad problem to have and I’m not complaining. (Folks from minority groups often suffer the opposite bias despite their hard-earned credentials, and that sucks.)

In this post I’ll offer an incomplete list of programming topics that people often wrongly assume that I know. I’m not saying you don’t need to learn them — or that I don’t know other useful things. But since I’m not in a vulnerable position myself right now, I can be honest about this.

Here’s why I think it’s important.


First, there is often an unrealistic expectation that an experienced engineer knows every technology in their field. Have you seen a “learning roadmap” that consists of a hundred libraries and tools? It’s useful — but intimidating.

What’s more, no matter how experienced you get, you may still find yourself switching between feeling capable, inadequate (“Impostor syndrome”), and overconfident (“Dunning–Kruger effect”). It depends on your environment, job, personality, teammates, mental state, time of day, and so on.

Experienced developers sometimes open up about their insecurities to encourage beginners. But there’s a world of difference between a seasoned surgeon who still gets the jitters and a student holding their first scalpel!

Hearing how “we’re all junior developers” can be disheartening and sound like empty talk to the learners faced with an actual gap in knowledge. Feel-good confessions from well-intentioned practitioners like me can’t bridge it.

Still, even experienced engineers have many knowledge gaps. This post is about mine, and I encourage those who can afford similar vulnerability to share their own. But let’s not devalue our experience while we do that.

We can admit our knowledge gaps, may or may not feel like impostors, and still have deeply valuable expertise that takes years of hard work to develop.


With that disclaimer out of the way, here’s just a few things I don’t know:

  • Unix commands and Bash. I can ls and cd but I look up everything else. I get the concept of piping but I’ve only used it in simple cases. I don’t know how to use xargs to create complex chains, or how to compose and redirect different output streams. I also never properly learned Bash so I can only write very simple (and often buggy) shell scripts.

  • Low-level languages. I understand Assembly lets you store things in memory and jump around the code but that’s about it. I wrote a few lines of C and understand what a pointer is, but I don’t know how to use malloc or other manual memory management techniques. Never played with Rust.

  • Networking stack. I know computers have IP addresses, and DNS is how we resolve hostnames. I know there’s low level protocols like TCP/IP to exchange packets that (maybe?) ensure integrity. That’s it — I’m fuzzy on details.

  • Containers. I have no idea about how to use Docker or Kubernetes. (Are those related?) I have a vague idea that they let me spin up a separate VM in a predictable way. Sounds cool but I haven’t tried it.

  • Serverless. Also sounds cool. Never tried it. I don’t have a clear idea of how that model changes backend programming (if it does at all).

  • Microservices. If I understand correctly, this just means “many API endpoints talking to each other”. I don’t know what the practical advantages or downsides of this approach are because I haven’t worked with it.

  • Python. I feel bad about this one — I have worked with Python for several years at some point and I’ve never bothered to actually learn it. There are many things there like import behavior that are completely opaque to me.

  • Node backends. I understand how to run Node, used some APIs like fs for build tooling, and can set up Express. But I’ve never talked from Node to a database and don’t really know how to write a backend in it. I’m also not familiar with React frameworks like Next beyond a “hello world”.

  • Native platforms. I tried learning Objective C at some point but it didn’t work out. I haven’t learned Swift either. Same about Java. (I could probably pick it up though since I worked with C#.)

  • Algorithms. The most you’ll get out of me is bubble sort and maybe quicksort on a good day. I can probably do simple graph traversing tasks if they’re tied to a particular practical problem. I understand the O(n) notation but my understanding isn’t much deeper than “don’t put loops inside loops”.

  • Functional languages. Unless you count JavaScript, I’m not fluent in any traditionally functional language. (I’m only fluent in C# and JavaScript — and I already forgot most of C#.) I struggle to read either LISP-inspired (like Clojure), Haskell-inspired (like Elm), or ML-inspired (like OCaml) code.

  • Functional terminology. Map and reduce is as far as I go. I don’t know monoids, functors, etc. I know what a monad is but maybe that’s an illusion.

  • Modern CSS. I don’t know Flexbox or Grid. Floats are my jam.

  • CSS Methodologies. I used BEM (meaning the CSS part, not the original BEM) but that’s all I know. I haven’t tried OOCSS or other methodologies.

  • SCSS / Sass. Never got to learn them.

  • CORS. I dread these errors! I know I need to set up some headers to fix them but I’ve wasted hours here in the past.

  • HTTPS / SSL. Never set it up. Don’t know how it works beyond the idea of private and public keys.

  • GraphQL. I can read a query but I don’t really know how to express stuff with nodes and edges, when to use fragments, and how pagination works there.

  • Sockets. My mental model is they let computers talk to each other outside the request/response model but that’s about all I know.

  • Streams. Aside from Rx Observables, I haven’t worked with streams closely. I used old Node streams one or two times but always messed up error handling.

  • Electron. Never tried it.

  • TypeScript. I understand the concept of types and can read annotations but I’ve never written it. The few times I tried, I ran into difficulties.

  • Deployment and devops. I can manage to send some files over FTP or kill some processes but that’s the limit of my devops skills.

  • Graphics. Whether it’s canvas, SVG, WebGL or low-level graphics, I’m not productive in it. I get the overall idea but I’d need to learn the primitives.

Of course this list is not exhaustive. There are many things that I don’t know.


It might seem like a strange thing to discuss. It even feels wrong to write it. Am I boasting of my ignorance? My intended takeaway from this post is that:

  • Even your favorite developers may not know many things that you know.

  • Regardless of your knowledge level, your confidence can vary greatly.

  • Experienced developers have valuable expertise despite knowledge gaps.

I’m aware of my knowledge gaps (at least, some of them). I can fill them in later if I become curious or if I need them for a project.

This doesn’t devalue my knowledge and experience. There’s plenty of things that I can do well. For example, learning technologies when I need them.

Update: I also wrote about a few things that I know.

Preparing for a Tech Talk, Part 1: Motivation

I’ve done a few tech talks that I think went well.

Sometimes people ask me how I prepare for a talk. For every speaker, the answer is very personal. I’m just sharing what works for me.

This is the first post in a series where I explain my process preparing for a tech talk — from conceiving the idea to the actual day of the presentation:


In this post, I will only focus on the first step: why and how I pick a topic. It’s not rich in practical tips but might help you ask yourself the right questions.


What motivates you to give a talk?

Maybe giving talks is a part of your current job. Maybe you want to gain more recognition in the industry so you can land a better job or get a raise. Maybe you’re out there to bring more attention to your hobby or work project.

We’ll call these motivations external. They are about what other people think of you and your work. But if you already had all the respect and money that you wanted, would you still choose to give a talk? Why?

Maybe you find it rewarding to teach people. Maybe you enjoy learning, and giving a talk is a nice excuse to dig deeper. Maybe you want to start or change the conversation about a topic. Maybe you want to amplify or critique an idea.

Such internal motivations aren’t a proxy for another desire like professional recognition. These are the things that have intrinsic value to you. Different people are driven by different internal motivations. It’s helpful to be aware of yours. You can sometimes trace them all the way back to your childhood.

For example, here’s mine:

  • I enjoy sharing ideas that inspire me. Sometimes, an idea transforms the way I think. It opens many doors that I didn’t even know existed. But it’s lonely behind those doors. I want others to join me so that they can show me even more interesting doors inside. For me, a talk is a way to collect, curate, and amplify ideas that I find tasteful. (As a teenager I made mixtapes for crushes with no interest in my music taste. Now I do talks! Life, uh, finds a way.)

  • I enjoy re-explaining things in a simpler way. When I understand an idea, I get a very pleasant feeling — better than eating sweets. But learning doesn’t come easy to me. So when I finally “get” something, I want to share that feeling with the people who are still struggling. I try to remember what it was like before the a-ha moment to help others “make the jump” while watching my talks. (I was also insufferable as a child because I insisted that everyone asks me questions. A talk is a more productive way to channel that energy.)


Combining these two internal motivations gives me a recipe for a personally satisfying talk: share an inspiring idea by re-explaining it in a simpler way.


That is my formula. Yours might be different — think about it! Which talks made you feel in a special way? What are the structural similarities between them? (We’ll discuss the talk structure more in the next posts in this series.)

Luna Lovegood invoking a Patronus Charm. Image © 2007 Warner Bros. Ent

Giving a talk that’s aligned with your motivations is helpful in several ways:

  1. It’s easier to pick a topic. My formula is “explain an inspiring idea and why you should care about it”. I can create talk proposals by applying this formula to any interesting concept that I learned. I’ll always have something to talk about as long as I’m listening to smart people with good ideas that deserve more exposure. There are many other possible formulas — find yours.

  2. It’s less scary on stage. I get terrified 30 seconds before the talk but the moment I start talking, I’m in my element. The drive to share an inspiring idea overtakes the fear of being judged or doing something wrong. (Of course, this only works with good preparation which we’ll talk about in the next posts.)

  3. It’s more convincing. I can’t phrase it better than Sophie did: if you’re enthusiastic about a topic, you can get the audience to care too. Enthusiasm doesn’t necessarily mean being loud or waving hands. Even if you’re calm, people can feel when there’s an emotional conviction behind a talk. (This is also why we can vibe to a song even if we don’t understand the words.)


There’s one more reason it helps when you’re genuinely excited about a topic. Feeling that you’re a part of something bigger does wonders for confidence.

My talks aren’t about me — they’re about an idea, and I’m just a messenger. Thousands of people on the livestream and in the audience aren’t really there to judge me (even if they think so). They came to experience the idea that I brought to share. My role is just to be a conduit from one mind to another. A lot of nerves and pressure from the talks disappeared after internalizing this.


Finding a formula that’s consistent with your motivations helps you establish your own voice. But how do you find a specific topic to which you can apply it?

In my experience, good talks start as conversations. Somebody explains an idea to me, and then I try to explain it to someone else. I talk about it to a dozen people, and eventually I find explanations that “click”. Sometimes there’s a thought that seems neglected or misunderstood, and I try to get individual people to see it in a different light.

For me, a talk is just a way to generalize those conversations and make them one-to-many rather than one-to-one. It’s like a “library” you extract out of the “application code” of many in-person and social media conversations.

So if you want to give a great talk, talking to people is a good way to start.

Hermione Granger making a potion. Vials have text imposed on top: "motivations" and "conversations". Cauldron is a metaphor for your talk. Image © 2001 Warner Bros. Ent


In this post, I described the framework that I find helpful for thinking about talk ideas. Again, I want to emphasize I’m just sharing what works for me — there are many kinds of talks and your outlook on this may be very different.

In the next posts in this series, I will talk about preparing the talk outline, slides, rehearsing the talks, and what I do on the day of the presentation.

Next in this series: Preparing for a Tech Talk, Part 2: What, Why, and How.

Why Do React Hooks Rely on Call Order?

At React Conf 2018, the React team presented the Hooks proposal.

If you’d like to understand what Hooks are and what problems they solve, check out our talks introducing them and my follow-up article addressing common misconceptions.

Chances are you won’t like Hooks at first:

Negative HN comment

They’re like a music record that grows on you only after a few good listens:

Positive HN comment from the same person four days later

When you read the docs, don’t miss the most important page about building your own Hooks! Too many people get fixated on some part of our messaging they disagree with (e.g. that learning classes is difficult) and miss the bigger picture behind Hooks. And the bigger picture is that Hooks are like functional mixins that let you create and compose your own abstractions.

Hooks are influenced by some prior art but I haven’t seen anything quite like them until Sebastian shared his idea with the team. Unfortunately, it’s easy to overlook the connection between the specific API choices and the valuable properties unlocked by this design. With this post I hope to help more people understand the rationale for the most controversial aspect of Hooks proposal.

The rest of this post assumes you know the useState() Hook API and how to write a custom Hook. If you don’t, check out the earlier links. Also, keep in mind Hooks are experimental and you don’t have to learn them right now!

(Disclaimer: this is a personal post and doesn’t necessarily reflect the opinions of the React team. It’s large, the topic is complex, and I may have made mistakes somewhere.)


The first and probably the biggest shock when you learn about Hooks is that they rely on persistent call index between re-renders. This has some implications.

This decision is obviously controversial. This is why, against our principles, we only published this proposal after we felt the documentation and talks describe it well enough for people to give it a fair chance.

If you’re concerned about some aspects of the Hooks API design, I encourage you to read Sebastian’s full response to the 1,000+ comment RFC discussion. It is thorough but also quite dense. I could probably turn every paragraph of this comment into its own blog post. (In fact, I already did that once!)

There is one specific part that I’d like to focus on today. As you may recall, each Hook can be used in a component more than once. For example, we can declare multiple state variables by calling useState() repeatedly:

function Form() {
  const [name, setName] = useState('Mary');              // State variable 1
  const [surname, setSurname] = useState('Poppins');     // State variable 2
  const [width, setWidth] = useState(window.innerWidth); // State variable 3

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  });

  function handleNameChange(e) {
    setName(e.target.value);
  }

  function handleSurnameChange(e) {
    setSurname(e.target.value);
  }

  return (
    <>
      <input value={name} onChange={handleNameChange} />
      <input value={surname} onChange={handleSurnameChange} />
      <p>Hello, {name} {surname}</p>
      <p>Window width: {width}</p>
    </>
  );
}

Note that we use array destructuring syntax to name useState() state variables but these names are not passed to React. Instead, in this example React treats name as “the first state variable”, surname as “the second state variable”, and so on. Their call index is what gives them a stable identity between re-renders. This mental model is well-described in this article.

On a surface level, relying on the call index just feels wrong. A gut feeling is a useful signal but it can be misleading — especially if we haven’t fully internalized the problem we’re solving. In this post, I’ll take a few commonly suggested alternative designs for Hooks and show where they break down.


This post won’t be exhaustive. Depending on how granular you’re counting, we’ve seen from a dozen to hundreds of different alternative proposals. We’ve also been thinking about alternative component APIs for the last five years.

Blog posts like this are tricky because even if you cover a hundred alternatives, somebody can tweak one and say: “Ha, you didn’t think of that!”

In practice, different alternative proposals tend to overlap in their downsides. Rather than enumerate all the suggested APIs (which would take me months), I’ll demonstrate the most common flaws with specific examples. Categorizing other possible APIs by these problems could be an exercise to the reader. 🧐

That is not to say that Hooks are flawless. But once you get familiar with the flaws of other solutions, you might find that the Hooks design makes some sense.


Flaw #1: Can’t Extract a Custom Hook

Surprisingly, many alternative proposals don’t allow custom Hooks at all. Perhaps we didn’t emphasize custom Hooks enough in the “motivation” docs. It’s difficult to do until the primitives are well-understood. So it’s a chicken-and-egg problem. But custom Hooks are largely the point of the proposal.

For example, an alternative banned multiple useState() calls in a component. You’d keep state in one object. That works for classes, right?

function Form() {
  const [state, setState] = useState({
    name: 'Mary',
    surname: 'Poppins',
    width: window.innerWidth,
  });
  // ...
}

To be clear, Hooks do allow this style. You don’t have to split your state into a bunch of state variables (see our recommendations in the FAQ).

But the point of supporting multiple useState() calls is so that you can extract parts of stateful logic (state + effects) out of your components into custom Hooks which can also independently use local state and effects:

function Form() {
  // Declare some state variables directly in component body
  const [name, setName] = useState('Mary');
  const [surname, setSurname] = useState('Poppins');

  // We moved some state and effects into a custom Hook
  const width = useWindowWidth();
  // ...
}

function useWindowWidth() {
  // Declare some state and effects in a custom Hook
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    // ...
  });
  return width;
}

If you only allow one useState() call per component, you lose the ability of custom Hooks to introduce local state. Which is the point of custom Hooks.

Flaw #2: Name Clashes

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.

There are a few variations on this idea, but they roughly look like this:

// ⚠️ This is NOT the React Hooks API
function Form() {
  // We pass some kind of state key to useState()
  const [name, setName] = useState('name');
  const [surname, setSurname] = useState('surname');
  const [width, setWidth] = useState('width');
  // ...

This tries to avoid reliance on the call index (yay explicit keys!) but introduces another problem — name clashes.

Granted, you probably won’t be tempted to call useState('name') twice in the same component except by mistake. This can happen accidentally but we could argue that about any bug. However, it’s quite likely that when you work on a custom Hook, you’ll want to add or remove state variables and effects.

With this proposal, any time you add a new state variable inside a custom Hook, you risk breaking any components that use it (directly or transitively) because they might already use the same name for their own state variables.

This is an example of an API that’s not optimized for change. The current code might always look “elegant”, but it is very fragile to changes in requirements. We should learn from our mistakes.

The actual Hooks proposal solves this by relying on the call order: even if two Hooks use a name state variable, they would be isolated from each other. Every useState() call gets its own “memory cell”.

There are still a few other ways we could work around this flaw but they also have their own issues. Let’s explore this problem space more closely.

Flaw #3: Can’t Call the Same Hook Twice

Another variation of the “keyed” useState proposal is to use something like Symbols. Those can’t clash, right?

// ⚠️ This is NOT the React Hooks API
const nameKey = Symbol();
const surnameKey = Symbol();
const widthKey = Symbol();

function Form() {
  // We pass some kind of state key to useState()
  const [name, setName] = useState(nameKey);
  const [surname, setSurname] = useState(surnameKey);
  const [width, setWidth] = useState(widthKey);
  // ...

This proposal seems to work for extracting the useWindowWidth() Hook:

// ⚠️ This is NOT the React Hooks API
function Form() {
  // ...
  const width = useWindowWidth();
  // ...
}

/*********************
 * useWindowWidth.js *
 ********************/
const widthKey = Symbol();

function useWindowWidth() {
  const [width, setWidth] = useState(widthKey);
  // ...
  return width;
}

But if we attempt to extract input handling, it would fail:

// ⚠️ This is NOT the React Hooks API
function Form() {
  // ...
  const name = useFormInput();
  const surname = useFormInput();
  // ...
  return (
    <>
      <input {...name} />
      <input {...surname} />
      {/* ... */}
    </>    
  )
}

/*******************
 * useFormInput.js *
 ******************/
const valueKey = Symbol();

function useFormInput() {
  const [value, setValue] = useState(valueKey);
  return {
    value,
    onChange(e) {
      setValue(e.target.value);
    },
  };
}

(I’ll admit this useFormInput() Hook isn’t particularly useful but you could imagine it handling things like validation and dirty state flag a la Formik.)

Can you spot the bug?

We’re calling useFormInput() twice but our useFormInput() always calls useState() with the same key. So effectively we’re doing something like:

  const [name, setName] = useState(valueKey);
  const [surname, setSurname] = useState(valueKey);

And this is how we get a clash again.

The actual Hooks proposal doesn’t have this problem because each call to useState() gets its own isolated state. Relying on a persistent call index frees us from worrying about name clashes.

Flaw #4: The Diamond Problem

This is technically the same flaw as the previous one but it’s worth mentioning for its notoriety. It’s even described on Wikipedia. (Apparently, it’s sometimes called “the deadly diamond of death” — cool beans.)

Our own mixin system suffered from it.

Two custom Hooks like useWindowWidth() and useNetworkStatus() might want to use the same custom Hook like useSubscription() under the hood:

function StatusMessage() {
  const width = useWindowWidth();
  const isOnline = useNetworkStatus();
  return (
    <>
      <p>Window width is {width}</p>
      <p>You are {isOnline ? 'online' : 'offline'}</p>
    </>
  );
}

function useSubscription(subscribe, unsubscribe, getValue) {
  const [state, setState] = useState(getValue());
  useEffect(() => {
    const handleChange = () => setState(getValue());
    subscribe(handleChange);
    return () => unsubscribe(handleChange);
  });
  return state;
}

function useWindowWidth() {
  const width = useSubscription(
    handler => window.addEventListener('resize', handler),
    handler => window.removeEventListener('resize', handler),
    () => window.innerWidth
  );
  return width;
}

function useNetworkStatus() {
  const isOnline = useSubscription(
    handler => {
      window.addEventListener('online', handler);
      window.addEventListener('offline', handler);
    },
    handler => {
      window.removeEventListener('online', handler);
      window.removeEventListener('offline', handler);
    },
    () => navigator.onLine
  );
  return isOnline;
}

This is a completely valid use case. It should be safe for a custom Hook author to start or stop using another custom Hook without worrying whether it is “already used” somewhere in the chain. In fact, you can never know the whole chain unless you audit every component using your Hook on every change.

(As a counterexample, the legacy React createClass() mixins did not let you do this. Sometimes you’d have two mixins that both do exactly what you need but are mutually incompatible due to extending the same “base” mixin.)

This is our “diamond”: 💎

       / useWindowWidth()   \                   / useState()  🔴 Clash
Status                        useSubscription() 
       \ useNetworkStatus() /                   \ useEffect() 🔴 Clash

Reliance on the persistent call order naturally resolves it:

                                                 / useState()  ✅ #1. State
       / useWindowWidth()   -> useSubscription()                    
      /                                          \ useEffect() ✅ #2. Effect
Status                         
      \                                          / useState()  ✅ #3. State
       \ useNetworkStatus() -> useSubscription()
                                                 \ useEffect() ✅ #4. Effect

Function calls don’t have a “diamond” problem because they form a tree. 🎄

Flaw #5: Copy Paste Breaks Things

Maybe we could salvage the keyed state proposal by introducing some sort of namespacing. There are a few different ways to do it.

One way could be to isolate state keys with closures. This would require you to “instantiate” custom Hooks and add a function wrapper around each of them:

/*******************
 * useFormInput.js *
 ******************/
function createUseFormInput() {
  // Unique per instantiation
  const valueKey = Symbol();  

  return function useFormInput() {
    const [value, setValue] = useState(valueKey);
    return {
      value,
      onChange(e) {
        setValue(e.target.value);
      },
    };
  }
}

This approach is rather heavy-handed. One of the design goals of Hooks is to avoid the deeply nested functional style that is prevalent with higher-order components and render props. Here, we have to “instantiate” any custom Hook before its use — and use the resulting function exactly once in the body of a component. This isn’t much simpler than calling Hooks unconditionally.

Additionally, you have to repeat every custom Hook used in a component twice. Once in the top level scope (or inside a function scope if we’re writing a custom Hook), and once at the actual call site. This means you have to jump between the rendering and top-level declarations even for small changes:

// ⚠️ This is NOT the React Hooks API
const useNameFormInput = createUseFormInput();
const useSurnameFormInput = createUseFormInput();

function Form() {
  // ...
  const name = useNameFormInput();
  const surname = useNameFormInput();
  // ...
}

You also need to be very precise with their names. You would always have “two levels” of names — factories like createUseFormInput and the instantiated Hooks like useNameFormInput and useSurnameFormInput.

If you call the same custom Hook “instance” twice you’d get a state clash. In fact, the code above has this mistake — have you noticed? It should be:

  const name = useNameFormInput();
  const surname = useSurnameFormInput(); // Not useNameFormInput!

These problems are not insurmountable but I would argue that they add more friction than following the Rules of Hooks.

Importantly, they break the expectations of copy-paste. Extracting a custom Hook without an extra closure wrapper still works with this approach but only until you call it twice. (Which is when it creates a conflict.) It’s unfortunate when an API seems to work but then forces you to Wrap All the Things™️ once you realize there is a conflict somewhere deep down the chain.

Flaw #6: We Still Need a Linter

There is another way to avoid conflicts with keyed state. If you know about it, you were probably really annoyed I still haven’t acknowledged it! Sorry.

The idea is that we could compose keys every time we write a custom Hook. Something like this:

// ⚠️ This is NOT the React Hooks API
function Form() {
  // ...
  const name = useFormInput('name');
  const surname = useFormInput('surname');
  // ...
  return (
    <>
      <input {...name} />
      <input {...surname} />
      {/* ... */}
    </>    
  )
}

function useFormInput(formInputKey) {
  const [value, setValue] = useState('useFormInput(' + formInputKey + ').value');
  return {
    value,
    onChange(e) {
      setValue(e.target.value);
    },
  };
}

Out of different alternatives, I dislike this approach the least. I don’t think it’s worth it though.

Code passing non-unique or badly composed keys would accidentally work until a Hook is called multiple times or clashes with another Hook. Worse, if it’s meant to be conditional (we’re trying to “fix” the unconditional call requirement, right?), we might not even encounter the clashes until later.

Remembering to pass keys through all layers of custom Hooks seems fragile enough that we’d want to lint for that. They would add extra work at runtime (don’t forget they’d need to serve as keys), and each of them is a paper cut for bundle size. But if we have to lint anyway, what problem did we solve?

This might make sense if conditionally declaring state and effects was very desirable. But in practice I find it confusing. In fact, I don’t recall anyone ever asking to conditionally define this.state or componentDidMount either.

What does this code mean exactly?

// ⚠️ This is NOT the React Hooks API
function Counter(props) {
  if (props.isActive) {
    const [count, setCount] = useState('count');
    return (
      <p onClick={() => setCount(count + 1)}>
        {count}
      </p>;
    );
  }
  return null;
}

Is count preserved when props.isActive is false? Or does it get reset because useState('count') wasn’t called?

If conditional state gets preserved, what about an effect?

// ⚠️ This is NOT the React Hooks API
function Counter(props) {
  if (props.isActive) {
    const [count, setCount] = useState('count');
    useEffect(() => {
      const id = setInterval(() => setCount(c => c + 1), 1000);
      return () => clearInterval(id);
    }, []);
    return (
      <p onClick={() => setCount(count + 1)}>
        {count}
      </p>;
    );
  }
  return null;
}

It definitely can’t run before props.isActive is true for the first time. But once it becomes true, does it ever stop running? Does the interval reset when props.isActive flips to false? If it does, it’s confusing that effect behaves differently from state (which we said wouldn’t reset). If the effect keeps running, it’s confusing that if outside the effect doesn’t actually make the effect conditional. Didn’t we say we wanted conditional effects?

If the state does get reset when we don’t “use” it during a render, what happens if multiple if branches contain useState('count') but only one runs at any given time? Is that valid code? If our mental model is a “map with keys”, why do things “disappear” from it? Would the developer expect an early return from a component to reset all state after it? If we truly wanted to reset the state, we could make it explicit by extracting a component:

function Counter(props) {
  if (props.isActive) {
    // Clearly has its own state
    return <TickingCounter />;
  }
  return null;
}

That would probably become the “best practice” to avoid these confusing questions anyway. So whichever way you choose to answer them, I think the semantics of conditionally declaring state and effects itself end up weird enough that you might want to lint against it.

If we have to lint anyway, the requirement to correctly compose keys becomes “dead weight”. It doesn’t buy us anything we actually want to do. However, dropping that requirement (and going back to the original proposal) does buy us something. It makes copy-pasting component code into a custom Hook safe without namespacing it, reduces bundle size paper cuts from keys and unlocks a slightly more efficient implementation (no need for Map lookups).

Small things add up.

Flaw #7: Can’t Pass Values Between Hooks

One of the best features of Hooks is that you can pass values between them.

Here is a hypothetical example of a message recipient picker that shows whether the currently chosen friend is online:

const friendList = [
  { id: 1, name: 'Phoebe' },
  { id: 2, name: 'Rachel' },
  { id: 3, name: 'Ross' },
];

function ChatRecipientPicker() {
  const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);

  return (
    <>
      <Circle color={isRecipientOnline ? 'green' : 'red'} />
      <select
        value={recipientID}
        onChange={e => setRecipientID(Number(e.target.value))}
      >
        {friendList.map(friend => (
          <option key={friend.id} value={friend.id}>
            {friend.name}
          </option>
        ))}
      </select>
    </>
  );
}

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  const handleStatusChange = (status) => setIsOnline(status.isOnline);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });
  return isOnline;
}

When you change the recipient, our useFriendStatus() Hook would unsubscribe from the previous friend’s status, and subscribe to the next one.

This works because we can pass the return value of the useState() Hook to the useFriendStatus() Hook:

  const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);

Passing values between Hooks is very powerful. For example, React Spring lets you create a trailing animation of several values “following” each other:

  const [{ pos1 }, set] = useSpring({ pos1: [0, 0], config: fast });
  const [{ pos2 }] = useSpring({ pos2: pos1, config: slow });
  const [{ pos3 }] = useSpring({ pos3: pos2, config: slow });

(Here’s a demo.)

Proposals that put Hook initialization into default argument values or that write Hooks in a decorator form make it difficult to express this kind of logic.

If calling Hooks doesn’t happen in the function body, you can no longer easily pass values between them, transform those values without creating many layers of components, or add useMemo() to memoize an intermediate computation. You also can’t easily reference these values in effects because they can’t capture them in a closure. There are ways to work around these issues with some convention but they require you to mentally “match up” inputs and outputs. This is tricky and violates React’s otherwise direct style.

Passing values between Hooks is at the heart of our proposal. Render props pattern was the closest you could get to it without Hooks, but you couldn’t get full benefits without something like Component Component which has a lot of syntactic noise due to a “false hierarchy”. Hooks flatten that hierarchy to passing values — and function calls is the simplest way to do that.

Flaw #8: Too Much Ceremony

There are many proposals that fall under this umbrella. Most attempt to avoid the perceived dependency of Hooks on React. There is a wide variety of ways to do it: by making built-in Hooks available on this, making them an extra argument you have to pass through everything, and so on.

I think Sebastian’s answer addresses this way better than I could describe so I encourage you to check out its first section (“Injection Model”).

I’ll just say there is a reason programmers tend to prefer try / catch for error handling to passing error codes through every function. It’s the same reason why we prefer ES Modules with import (or CommonJS require) to AMD’s “explicit” definitions where require is passed to us.

// Anyone miss AMD?
define(['require', 'dependency1', 'dependency2'], function (require) {
  var dependency1 = require('dependency1'),
  var dependency2 = require('dependency2');
  return function () {};
});

Yes, AMD may be more “honest” to the fact that modules aren’t actually synchronously loaded in a browser environment. But once you learn about that, writing the define sandwich becomes a mindless chore.

try / catch, require, and React Context API are pragmatic examples of how we want to have some “ambient” handler available to us instead of explicitly threading it through every level — even if in general we value explicitness. I think the same is true for Hooks.

This is similar to how, when we define components, we just grab Component from React. Maybe our code would be more decoupled from React if we exported a factory for every component instead:

function createModal(React) {
  return class Modal extends React.Component {
    // ...
  };
}

But in practice this ends up being just an annoying indirection. When we actually want to stub React with something else, we can always do that at the module system level instead.

The same applies to Hooks. Still, as Sebastian’s answer mentions, it is technically possible to “redirect” Hooks exported from react to a different implementation. (One of my previous posts mentions how.)

Another way to impose more ceremony is by making Hooks monadic or adding a first-class concept like React.createHook(). Aside from the runtime overhead, any solution that adds wrappers loses a huge benefit of using plain functions: they are as easy to debug as it gets.

Plain functions let you step in and out with a debugger without any library code in the middle, and see exactly how values flow inside your component body. Indirections make this difficult. Solutions similar in spirit to either higher-order components (“decorator” Hooks) or render props (e.g. adopt proposal or yielding from generators) suffer from the same problem. Indirections also complicate static typing.


As I mentioned earlier, this post doesn’t aim to be exhaustive. There are other interesting problems with different proposals. Some of them are more obscure (e.g. related to concurrency or advanced compilation techniques) and might be a topic for another blog post in the future.

Hooks aren’t perfect either, but it’s the best tradeoff we could find for solving these problems. There are things we still need to fix, and there exist things that are more awkward with Hooks than classes. That is also a topic for another blog post.

Whether I covered your favorite alternative proposal or not, I hope this writeup helped shed some light on our thinking process and the criteria we consider when choosing an API. As you can see, a lot of it (such as making sure that copy-pasting, moving code, adding and removing dependencies works as expected) has to do with optimizing for change. I hope that React users will appreciate these aspects.

Optimized for Change

What makes a great API?

Good API design is memorable and unambiguous. It encourages readable, correct and performant code, and helps developers fall into the pit of success.

I call these design aspects “first order” because they are the first things a library developer tends to focus on. You might have to compromise on some of them and make tradeoffs but at least they’re always on your mind.

However, unless you’re sending a rover to Mars, your code will probably change over time. And so will the code of your API consumers.

The best API designers I know don’t stop at the “first order” aspects like readability. They dedicate just as much, if not more, effort to what I call the “second order” API design: how code using this API would evolve over time.

A slight change in requirements can make the most elegant code fall apart.

Great APIs anticipate that. They anticipate that you’ll want to move some code. Copy and paste some part. Rename it. Unify special cases into a generic reusable helper. Unwind an abstraction back into specific cases. Add a hack. Optimize a bottleneck. Throw away a part and start it anew. Make a mistake. Navigate between the cause and the effect. Fix a bug. Review the fix.

Great APIs not only let you fall into a pit of success, but help you stay there.

They’re optimized for change.

How Does setState Know What to Do?

When you call setState in a component, what do you think happens?

import React from 'react';
import ReactDOM from 'react-dom';

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { clicked: false };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.setState({ clicked: true });
  }
  render() {
    if (this.state.clicked) {
      return <h1>Thanks</h1>;
    }
    return (
      <button onClick={this.handleClick}>
        Click me!
      </button>
    );
  }
}

ReactDOM.render(<Button />, document.getElementById('container'));

Sure, React re-renders the component with the next { clicked: true } state and updates the DOM to match the returned <h1>Thanks</h1> element.

Seems straightforward. But wait, does React do it? Or React DOM?

Updating the DOM sounds like something React DOM would be responsible for. But we’re calling this.setState(), not something from React DOM. And our React.Component base class is defined inside React itself.

So how can setState() inside React.Component update the DOM?

Disclaimer: Just like most other posts on this blog, you don’t actually need to know any of that to be productive with React. This post is for those who like to see what’s behind the curtain. Completely optional!


We might think that the React.Component class contains DOM update logic.

But if that were the case, how can this.setState() work in other environments? For example, components in React Native apps also extend React.Component. They call this.setState() just like we did above, and yet React Native works with Android and iOS native views instead of the DOM.

You might also be familiar with React Test Renderer or Shallow Renderer. Both of these testing strategies let you render normal components and call this.setState() inside them. But neither of them works with the DOM.

If you used renderers like React ART, you might also know that it’s possible to use more than one renderer on the page. (For example, ART components work inside a React DOM tree.) This makes a global flag or variable untenable.

So somehow React.Component delegates handling state updates to the platform-specific code. Before we can understand how this happens, let’s dig deeper into how packages are separated and why.


There is a common misconception that the React “engine” lives inside the react package. This is not true.

In fact, ever since the package split in React 0.14, the react package intentionally only exposes APIs for defining components. Most of the implementation of React lives in the “renderers”.

react-dom, react-dom/server, react-native, react-test-renderer, react-art are some examples of renderers (and you can build your own).

This is why the react package is useful regardless of which platform you target. All its exports, such as React.Component, React.createElement, React.Children utilities and (eventually) Hooks, are independent of the target platform. Whether you run React DOM, React DOM Server, or React Native, your components would import and use them in the same way.

In contrast, the renderer packages expose platform-specific APIs like ReactDOM.render() that let you mount a React hierarchy into a DOM node. Each renderer provides an API like this. Ideally, most components shouldn’t need to import anything from a renderer. This keeps them more portable.

What most people imagine as the React “engine” is inside each individual renderer. Many renderers include a copy of the same code — we call it the “reconciler”. A build step smooshes the reconciler code together with the renderer code into a single highly optimized bundle for better performance. (Copying code is usually not great for bundle size but the vast majority of React users only needs one renderer at a time, such as react-dom.)

The takeaway here is that the react package only lets you use React features but doesn’t know anything about how they’re implemented. The renderer packages (react-dom, react-native, etc) provide the implementation of React features and platform-specific logic. Some of that code is shared (“reconciler”) but that’s an implementation detail of individual renderers.


Now we know why both react and react-dom packages need to be updated for new features. For example, when React 16.3 added the Context API, React.createContext() was exposed on the React package.

But React.createContext() doesn’t actually implement the context feature. The implementation would need to be different between React DOM and React DOM Server, for example. So createContext() returns a few plain objects:

// A bit simplified
function createContext(defaultValue) {
  let context = {
    _currentValue: defaultValue,
    Provider: null,
    Consumer: null
  };
  context.Provider = {
    $$typeof: Symbol.for('react.provider'),
    _context: context
  };
  context.Consumer = {
    $$typeof: Symbol.for('react.context'),
    _context: context,
  };
  return context;
}

When you use <MyContext.Provider> or <MyContext.Consumer> in the code, it’s the renderer that decides how to handle them. React DOM might track context values in one way, but React DOM Server might do it differently.

So if you update react to 16.3+ but don’t update react-dom, you’d be using a renderer that isn’t yet aware of the special Provider and Consumer types. This is why an older react-dom would fail saying these types are invalid.

The same caveat applies to React Native. However, unlike React DOM, a React release doesn’t immediately “force” a React Native release. They have an independent release schedule. The updated renderer code is separately synced into the React Native repository once in a few weeks. This is why features become available in React Native on a different schedule than in React DOM.


Okay, so now we know that the react package doesn’t contain anything interesting, and the implementation lives in renderers like react-dom, react-native, and so on. But that doesn’t answer our question. How does setState() inside React.Component “talk” to the right renderer?

The answer is that every renderer sets a special field on the created class. This field is called updater. It’s not something you would set — rather, it’s something React DOM, React DOM Server or React Native set right after creating an instance of your class:

// Inside React DOM
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;

// Inside React DOM Server
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;

// Inside React Native
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;

Looking at the setState implementation in React.Component, all it does is delegate work to the renderer that created this component instance:

// A bit simplified
setState(partialState, callback) {
  // Use the `updater` field to talk back to the renderer!
  this.updater.enqueueSetState(this, partialState, callback);
}

React DOM Server might want to ignore a state update and warn you, whereas React DOM and React Native would let their copies of the reconciler handle it.

And this is how this.setState() can update the DOM even though it’s defined in the React package. It reads this.updater which was set by React DOM, and lets React DOM schedule and handle the update.


We know about classes now, but what about Hooks?

When people first look at the Hooks proposal API, they often wonder: how does useState “know what to do”? The assumption is that it’s more “magical” than a base React.Component class with this.setState().

But as we have seen today, the base class setState() implementation has been an illusion all along. It doesn’t do anything except forwarding the call to the current renderer. And useState Hook does exactly the same thing.

Instead of an updater field, Hooks use a “dispatcher” object. When you call React.useState(), React.useEffect(), or another built-in Hook, these calls are forwarded to the current dispatcher.

// In React (simplified a bit)
const React = {
  // Real property is hidden a bit deeper, see if you can find it!
  __currentDispatcher: null,

  useState(initialState) {
    return React.__currentDispatcher.useState(initialState);
  },

  useEffect(initialState) {
    return React.__currentDispatcher.useEffect(initialState);
  },
  // ...
};

And individual renderers set the dispatcher before rendering your component:

// In React DOM
const prevDispatcher = React.__currentDispatcher;
React.__currentDispatcher = ReactDOMDispatcher;
let result;
try {
  result = YourComponent(props);
} finally {
  // Restore it back
  React.__currentDispatcher = prevDispatcher;
}

For example, the React DOM Server implementation is here, and the reconciler implementation shared by React DOM and React Native is here.

This is why a renderer such as react-dom needs to access the same react package that you call Hooks from. Otherwise, your component won’t “see” the dispatcher! This may not work when you have multiple copies of React in the same component tree. However, this has always led to obscure bugs so Hooks force you to solve the package duplication before it costs you.

While we don’t encourage this, you can technically override the dispatcher yourself for advanced tooling use cases. (I lied about __currentDispatcher name but you can find the real one in the React repo.) For example, React DevTools will use a special purpose-built dispatcher to introspect the Hooks tree by capturing JavaScript stack traces. Don’t repeat this at home.

This also means Hooks aren’t inherently tied to React. If in the future more libraries want to reuse the same primitive Hooks, in theory the dispatcher could move to a separate package and be exposed as a first-class API with a less “scary” name. In practice, we’d prefer to avoid premature abstraction until there is a need for it.

Both the updater field and the __currentDispatcher object are forms of a generic programming principle called dependency injection. In both cases, the renderers “inject” implementations of features like setState into the generic React package to keep your components more declarative.

You don’t need to think about how this works when you use React. We’d like React users to spend more time thinking about their application code than abstract concepts like dependency injection. But if you’ve ever wondered how this.setState() or useState() know what to do, I hope this helps.