Hooked on Hooks
Introduction
Erin Zimmer introduces herself and the topic of her talk, "Hooked on Hooks," which delves into the world of React Hooks. She explains that the slides for her talk are available at hooks.ez.codes and outlines the key areas she'll cover: the purpose of hooks, the rules governing their usage, why those rules can be confusing, and practical examples using useRef, useState, and useEffect.
Why Hooks?
Erin discusses the motivation behind using hooks in React development. She highlights the preference for functional components over class-based components and outlines the advantages of the former, including their nature as pure functions, the absence of lifecycle methods, reduced boilerplate code, and enhanced tooling capabilities. However, she also points out the limitations of functional components, such as their inability to manage state, trigger re-renders, or utilize lifecycle methods directly.
Hooks to the Rescue
Erin emphasizes that hooks address the limitations of functional components. She acknowledges that hooks can appear confusing initially, lacking a clear mental model for understanding their mechanisms. She dissects the rules associated with hooks, often encountered as linting errors, and rephrases them for clarity, focusing on the importance of consistent hook call order within a component and the concept of hooks belonging to component instances.
Hooks, Closures, and State
Erin reveals the underlying mechanism of hooks: storing state within arrays using closures. She provides a concise explanation of closures in JavaScript, describing them as a mechanism for managing scope. She acknowledges that closures can be a challenging concept but assures the audience that practical examples will enhance their understanding.
Understanding useRef
Erin delves into useRef, the most fundamental React hook. She explains its primary function: storing state between renders. The most common use case for useRef, she notes, is obtaining references to HTML elements. Erin illustrates this with an example of a form where useRef is employed to capture input values. To further clarify, she introduces a simplified "Fake React" implementation to demonstrate how useRef works under the hood.
Introducing useState
Erin moves on to useState, explaining that useRef alone cannot trigger component re-renders, which are essential for reflecting state changes in the UI. useState, she says, addresses this limitation. Erin illustrates useState's functionality with an example of an inventory component that manages and displays a list of items. She revisits the "Fake React" implementation to break down the internal workings of useState, showing how it updates state and triggers re-renders.
The Need for useEffect
Erin introduces the challenges of managing side effects in functional components, such as adding global event listeners. She uses the example of implementing keyboard shortcuts for an inventory application, highlighting the issue of accumulating event listeners on each component re-render, leading to performance problems and conflicting state updates. This, she explains, necessitates a mechanism for adding and removing event listeners efficiently.
useEffect to the Rescue
Erin presents useEffect as the solution for managing side effects effectively. She breaks down useEffect's syntax, explaining the purpose of the effect callback, the dependencies array for handling stale closures, and the cleanup function for performing actions before the next effect execution. Using the keyboard shortcut example, she demonstrates how useEffect ensures that only one event listener is active at a time.
useEffect: Beyond Event Listeners
Erin emphasizes that useEffect's utility extends beyond event handling. It serves as a bridge between React components and external systems or APIs that are not inherently aware of React's rendering cycle. She lists common use cases for useEffect, including interacting with native APIs like setTimeout and setInterval, making HTTP requests, and working with third-party libraries.
useEffect: An Escape Hatch
Erin advises against using useEffect for tasks that React is already designed to handle, such as transforming data based on props or state changes. She reiterates that useEffect should be viewed as an escape hatch for situations where direct integration with React's rendering paradigm is not feasible or desirable. She provides an example of implementing sorting functionality, emphasizing that calculating the sorted list during rendering is a more appropriate approach than using useEffect to manage it.
Recap and Resources
Erin summarizes the key takeaways from her talk: hooks provide a way to manage state and side effects in functional components, useRef stores state persistently, useState enables state updates and component re-renders, and useEffect bridges the gap between React and external systems. She recommends resources for further exploration, including Swyx's work on React hooks and Dan Abramovsky's writings on JavaScript.
hi, so welcome to my talk, Hooked on Hooks.
As was just mentioned, this is my name, I work at Atlassian, you've probably heard of us.
We make Jira, and a bunch of other tools for helping teams work together.
This talk has got a bunch of code in it, so if you want to follow along on your own device, the slides are at hooks.ez.codes.
And if you're like, I don't need the slides, and then you get to the end and you're like, huh, I would have liked the slides, don't worry, the link's at the end as well.
All right, so what are we going to talk about today?
We don't know.
Oh.
Okay, what are we going to talk about today?
We're going to talk about why do we even need hooks, we're going to talk about the rules of hooks, why they're confusing, and what they're even about, and we're going to look at some examples with useRefuseState and useEffect.
Okay, so why hooks?
Why do we even want hooks?
The answer to that is very simple.
We need hooks because We like to write functional components.
For anybody who's newer to React, there's actually two ways to write components in React.
You can write a functional component using a function, or you can write a class based component.
And we've all decided that actually we like functional components better, so that's what we all do.
And that wasn't just an arbitrary decision, though.
There are a few reasons why we like functional components.
First of all, they are pure functions, which means if you put the same props in, you'll get the same HTML out, which makes it easier to reason about.
You don't have to worry about lifecycle methods.
So class based components have this method called render, that gets called when they render, but they also have a bunch of these other methods.
That are all called things like ComponentDidMount, or ComponentWillMount, or ComponentThoughtAboutMounting, and then it changed its mind.
I don't know, there's a bunch of them, I can't remember what they're called because I don't write class based components anymore.
And I needed to free up that brain space for important stuff, which of the Aes Sedai is Black Aja in the Wheel of Time series?
The critical stuff.
Functional components have less boilerplate.
When you're writing classes in JavaScript, you need to do a bunch of stuff to make the this keyword work the way you would expect it to.
It's very boring.
The React team says that functional components are easier to write tooling for, and I guess they'd know.
But there are a few problems with functional components.
They can't store state.
They can't trigger re renders, and they don't have lifecycle methods.
And I know I said that was an advantage, but sometimes you just really want to run some code after a component has already rendered, and you can't do that with a functional component.
You can now, because we have hooks!
So that's great!
Hooks solve all those problems for us.
The thing about hooks, though, is that they're a bit confusing.
The first time I looked at them, I was is this witchcraft?
They're, very opaque, right?
There's no kind of obvious mental model about how they work.
They just do stuff, and they've got these rules.
They mean something, or you might have seen these rules actually as a linting error.
And when they're a linting error, they're a bit clearer what's going on.
So they might say something like, React hook use ref is called conditionally.
React hooks must be called in the exact same order in every component render.
So I would actually rewrite these rules.
I would say rather than saying only call hooks at the top level, I would say always call hooks in the same order.
So within your component, you always need to call all of your hooks every time in the same order.
So you can't have your component, say, return early and not call some of its hooks sometimes.
You can't call a hook from inside an if loop.
An if statement, you can't call a hook inside a while loop?
When was the last time any of you wrote a while loop?
No?
Yeah.
More, it's been a while?
Nice.
Most, most importantly, you can't call them inside callback functions because they get called any old time, right?
So always in the same order.
Rather than saying only call hooks from React functions, I would say a hook belongs to an instance of a component, right?
So the order, it only matters from inside an instance of a component.
It doesn't matter what order the components get called in.
But your hook always needs to belong to a component, which means that you can only call hooks from inside components, or inside hooks that are called inside a component, or inside hooks that are called from hooks that are inside a component.
But there needs to be a component there somewhere.
That's what the rules mean, but, like, why?
Why do we have these rules?
And the answer to that is that hooks store state in arrays inside closures.
And if you're not super familiar with closures, that's alright, MDN's got your back.
A closure is the combination of a function bundled together or enclosed with the references to its surrounding state.
The lexical environment.
So if you weren't familiar with closures a minute ago, you're probably still not real sure what's going on.
And that's okay.
Closures are one of the hard things that you have to understand in JavaScript.
They are basically just how JavaScript manages scope.
And the, just is doing a lot of heavy lifting in that sentence, but rather than trying to explain them anymore, we'll just look at some examples and see how they work.
And hopefully that will clear things up a bit.
All right.
So let's start with useRef.
useRef is the most basic, I can't stand still, this is useRef is the most basic hook.
All it does is store state between renders.
And the most common use case for useRef is for grabbing references to HTML elements.
So for instance, we could have a form here where we want to add some stuff and we can do this using useRef.
Code looks something like this.
For each of the HTML elements that we want to reference to, we're going to call useRef and get a variable.
We're going to pass those variables into the ref prop of the elements, which then makes the variables point to those elements, and then we can access the, we can access those elements using the current property of those variables, so we can get the value using like nameRef dot current dot value.
So what does this look like under the hood?
All right, so here we have an implementation of Fake React, and down the bottom is our component, and we're going to have a look at a bunch of these, so I just want to make clear real quick, this isn't React code, this isn't what it looks like really, the aim here isn't to teach you what React is doing, the aim is to build a mental model that's going to allow you to reason about your own components, so we're going for usefulness over accuracy.
Alright, so the first time our component renders, we're going to create these, variables here.
We're going to create an array, and we're going to create an index that we can use to reference the array.
Then inside our component, we're going to call useRef.
And inside useRef, we're going to check to see if there's anything inside the first position of that array.
And we can do this because this useRef function is an inner scope inside this outer scope that is our fake React component.
And inside JavaScript, anything inside an inner scope can reference any of the variables inside the outer scope.
So we can grab that.
We can see that there's nothing inside the refs array.
So we're going to add an object with a current property.
Then we're just going to grab a reference.
We're going to increment our current index, and we're going to return that reference.
And when we return this reference, the mental model here isn't that we're passing the variable back, it's more like we're telling the component where that object is.
So you can think of it like a safety deposit box at a bank, where you've put something inside, and then you've got a key, and you can give that key to someone else, and they can go and open the box, they can look inside, they can see what's there, they can change what's there, but they can't change the box.
The box is still always going to stay there, like this object is always going to be the first element in this array.
Okay, then we're going to call useRef again.
This time, current index is one, so we're going to check to see if there's anything in the second position in the array, and there's not, so we'll add a new box there, we'll grab a reference to it, we'll increment our current index, and we'll return the ref.
Then we're going to declare our addItem function, and we're going to return our HTML.
And when we return our HTML, we're going to pass those variables into the ref props, So then they're going to point to those HTML elements.
And then after our component has finished rendering, we're just going to reset current index back to zero, so the next time it renders, we're going to start again at the beginning of the array.
Cool.
So then at some point, someone's going to hit that add button, and we're going to add an item, and we're going to call our handleAddItem function.
And the handleAddItem function is an inner scope inside an outer scope, which means it has access to the variables in the outer scope, even though that outer scope has gone away.
We still have access to those variables.
And this is what closures are, and this is why they're confusing.
So we've still got access to name ref and count ref there, and they still point to those, objects.
So we can grab the va the values, and pass them into our onSubmit function.
And that's it.
That's, useRef.
It's a hook, it's still a state, in an array, inside a closure.
Incidentally, as soon as we start doing this, our functional components are no longer pure functions.
So I guess you lose that advantage.
That's a trade off we've all decided is worth it.
Alright.
So now what if we want to start using the stuff we've submitted from our form?
So we want to, when we add our Apple, we want to display it on the screen.
We can't do this with useRef.
Because useRef can't make the component re-render, right?
All it can do is store up all the state, but we can't see it on the screen until the component re-render.
So we need a different hook for this.
And the hook for that is useState and the component, the code looks something like this.
We've got our inventory component, which is now orchestrating state between the child components, is storing the state using useState.
Our add item function is updating the state.
Using the setInventory function, we've got our addItem form, which is the component we were just using in our useRef example, and we've got an item list which displays a list of items, and how it works is not important yet.
So again we've got our fake react.
And we're going to create our array, and our index for accessing the array, and this time we've also got this flag called runComponent.
We're going to run our component, we're going to render our component, and it's going to call useState and pass in our initial state, which is an empty array.
So just like with useRef, the first thing we're going to do is check if there's anything in that first position in the array, and there's not.
So we're going to stick our initial value in there, and it now has an empty array.
Then we're going to do a whole bunch of stuff to figure out what we need to return back to the component.
So first we need the value, so we can just grab a reference for that.
Then we're going to create this variable called setterIndex, and we'll see why in a minute.
And we're going to create this setter function, which knows how to update the first element in the states array, and also the runComponent key.
And then we're going to increment currentIndex.
So the really, the reason that we needed the setterIndex variable is that if we'd used currentIndex directly, then the set of function would close over the current index.
It would have it inside its closure.
And when we incremented current index, it would now be pointing to the wrong position in the array and things would not work the way we wanted.
So instead we create setIndex and the set of function closes over set index and the value of set index doesn't change when we increment current index.
So everything works the way we want.
All right, and then we're gonna return our value and our set index.
I'll set a function.
Then we're going to declare our addItem function, and we're going to return our JSX.
When we return our JSX, we're adding this onSubmit handler to our form.
The onSubmit handler, to add that React is going to create an event listener, which is going to do a bunch of stuff.
And then we're going to return.
We're going to run our after function, which is going to reset current index.
Then at some point, someone's going to hit that add button, and it's going to call the onSubmit handler.
The onSubmit handler is going to call the listener.
The listener is going to call the handler that we passed in.
The handler that we passed in is the addItem function.
So the addItem function, again, is a closure that closes around the inventory and setInventory.
So it can call the setInventory function.
Which was the code that we had that we said did a bunch of stuff.
So inside the setup function, yes, the first thing we're going to do is check whether the new value that's being passed in is actually different to what we've already got.
Because if they're the same, we just don't need to do anything and we can skip all this.
But in our case they're different, because one's an empty array and the other is an array with an apple in it.
So we're going to run this code, we're going to grab that state, and we're going to set the run component flag to true.
Then we're going to pass control back to our handler, which is going to pass control back to our listener, and inside the listener we're going to check that flag, and it's true, so we're going to set it back to false, and then we're going to run our component.
And the reason we need this flag is that if we had just run the component again every time we updated a bit of state, we'd end up doing it a lot, right?
So if that handler had updated multiple pieces of state, we only want to run the component once, right?
So we do all of our state updates and then update the component.
So even if you update one piece of state multiple times, the component's only going to run with the latest value.
Cool.
So then where were we?
Oh, we're running our component again.
Yes.
So we're running our component again.
We're calling new state to grab our inventory and set inventory.
We're declaring our new add item, which is a new closure, which closes over the new inventory, and we're returning our JSX.
So that's it, that's useState.
It stores state in an array in a closure, and it re renders the component when that state changes.
Cool!
So what if we want to add some more functionality here?
We want to actually use these items, right?
So we want to be able to use some keyboard shortcuts.
If I press 1, I'm going to use an apple.
If I press 5, it's going to use a mushroom, and so on.
We can do that using window.addEventListener.
We need to use The window.addEventListener, we can't use an on keydown handler inside the component, because then it would only work when the component had focus.
So we want this to work no matter what on the page has focus, so we need to use a global EventListener.
So inside our EventListener here, we're going to find the key that is used, we're going to figure out which item that refers to, and we're going to decrement the count of that item.
And then we're going to update the list of items by calling setItems, which is the setInventory function from the parent.
So that's going to cause the parent component to re render, which is going to cause our child component to re render, because when a parent re renders, the child always re renders, which is going to cause it to call window.
AddEventListener again.
So now we've got two event listeners.
So now when we press a key, two event listeners are going to fire, and they're both going to update the list of items, and they're both going to call setItems, so they're going to cause the parent component to re render twice, which is going to cause the child component to re render twice, which is going to cause two new event listeners to be added.
And we are very quickly going to have a lot of event listeners.
Which is obviously going to be a performance problem, but it's worse than that.
Because each of these event listeners is a closure that's closing over the list of items, each one is closing over the list of items at the time that it was created, so they're all pointing to different lists of items now.
So not only are we going to have a lot of event listeners, they're all going to be sending through conflicting updates about what's going on with the list of items.
So this code doesn't work.
So what we need is a hook that will add the event listener just once, that will watch to see when the closure goes stale and we'll remove the event listener and add a brand new event listener that closes over the new value.
And that hook is useEffect.
So the code looks something like this.
useEffect is just a function which takes two arguments.
The first argument is the effect, is the thing that we want to happen.
So in our case, adding the event listener.
The second argument is the dependencies list, which is a list of variables which could cause the closure to go stale.
As a general rule of thumb, any variable that is used inside your callback that is declared outside of your callback probably needs to go on the dependencies list.
There are some exceptions to this for things that can't actually change.
My advice would be use ESLint, it will tell you what to put in the list [inaudible from audience].
That would be a weird place to look for it.
So when any of the items in the dependencies list changes, we call the effect again.
We run the effect again, which doesn't solve our problem because we're still going to end up with a lot of event listeners.
So the other piece here is that our callback can also return a cleanup function, and the cleanup function will get run before the effect runs again.
Alright, so the three bits we had there is the effect, which is what we want to happen, the dependencies, which is how we handle stale closures, and the cleanup function, which is going to get called before the effect gets called, like a second or third or whatever time.
Alright, so how do we implement that?
Again, we have an array and an index to access the array.
We're going to call our component, which is going to get passed in its props, a list of items, and the setItems key.
And then we're going to call useEffect.
And into useEffect, we're going to pass a callback function, which is going to be a function that knows how to add the purple event listener.
And the purple event listener is the one that closes around the list of items with three things in it.
And a list of dependencies, which is the list of items and the setItems function.
So we'll pass those in there.
Once again, we're going to check to see if there's anything in the first element in the array, and there's not, so we're going to create an object.
Which is going to point to our callback and also to the dependencies list.
Then we're just going to increment current index and pass control back to the components and the component is going to do its JSX-y thing.
Cool.
So the interesting part of useEffect all actually happens in the after part.
So effects always run asynchronously.
They always run after the component has rendered.
And inside here, the first thing that we need to do is to check whether we need to run the cleanup function.
So we're going to iterate through all of our effects, and we've only got one, so it's not going to take very long, thankfully.
And we're going to check to see if they need cleanup.
So we're going to check if the dependencies have changed, or if the component is unmounting.
The component's unmounting if it's being removed from the DOM, it's not going to render again.
In our case that's not true, we can just ignore it.
But we do need to check if the dependencies have changed.
Thanks, autoscroll.
So we're going to check first if our effect has a previous dependencies property, and it doesn't, so we're just going to return false immediately.
And the reason for this is that the first time the component renders, the effect hasn't run yet, so we know that we don't need to do a clean up.
So we can just skip that.
Then we need to check whether we want to run the effect.
So again, we're going to iterate through all of our one effect, and we're going to check.
First we're going to check, does it have a dependencies list?
Ours does, so this is false.
But in general, we need to check this, because if it comp the dependencies list is actually optional, and if it's not there, then the effect will run every time the component renders.
Which might sound like a waste of time, but sometimes you just need something to run after the component has rendered, right?
Perhaps you are rendering some HTML and you need to measure the size of an element.
That obviously has to happen after the element has rendered, otherwise the size is going to be real small.
Next we're going to check if there's a previous dependencies array.
We don't have one, so we can say yes, this is true.
The reason we check for this is that effects are guaranteed to run at least once.
So if this is the first time the component has rendered, we're always going to run the effect.
Then inside our effect, we're going to update our effect object here.
We're going to add that previous dependencies property, which is going to point to our current dependencies property.
We're going to call our callback, which is going to add that purple event listener.
And we're going to store the cleanup function, which is a function that knows how to get rid of the purple event listener.
And then we can get rid of those other props that we don't need anymore.
All right, and then, oh, order scroll, we need to reset current index again.
Cool, so that's, our effect has run for the first time, and then, at some point someone's going to press that key, it's going to call set items, which is going to cause the parent component to re render, which is going to cause our component to re render.
With a new list of items, there's only two items in the list now, and we're going to call useEffect.
And we're going to pass in, the callback function, which is a new callback function, which knows how to add the blue listener.
And this listener closes over the new dependencies list, which only has two items.
You can remember because blue and two rhyme.
This time there is something in that first element in the array, so we're just going to update the existing effect.
We're going to keep the variables that are already there, the previous dependencies and the cleanup function.
And we're going to add our new callback and our new list of dependencies.
And then we're going to increment current index and pass control back to the components.
Yes.
The component is going to do its JSX again.
And then we're going to do the fun stuff in our after component.
So again, we're going to iterate through our effect.
We're going to, again, it's not unmounting.
We don't need to worry about that.
But we need to check if the dependencies have changed.
This time we have a previous dependencies array, so instead we're going to go through, the dependencies array and the previous dependencies array, and make sure that they match each other.
And in this case they don't, because the first element in each array is different.
Is different from each other?
Are different from each other?
The first elements, they're different.
So this is going to return true.
So we need to do a cleanup and we have a cleanup function because the cleanup functions are optional.
So we're going to run our cleanup function and it's going to remove that purple event listener.
Then we're going to run, see, we need to run the effect again.
So this time we have a dependencies array, so that's false.
We have a previous dependencies array, so that's false.
We can run the dependencies change, we just did that, we know it's true.
So we know we need to update our effect.
And we're going to update our previous dependencies array to point to the new dependencies array.
We're going to call our callback, which is going to add the new blue event listener.
There is a button that I pressed that I should not press.
And we're going to store the blue cleanup function, and then we can get rid of those other properties we don't need anymore.
Cool.
And then we're going to reset current index, and that's done.
That's useEffect.
So a couple of things about the dependencies array you probably noticed from the code.
The effect runs at least once, and if there's no dependency array, it runs on every render.
The effect runs on every render.
Something that you might not have quite picked up from the code is that if you have an empty dependencies array, your effect will run exactly once.
That's because effects are guaranteed to run at least once, but if there's nothing in the dependencies array, it can never change.
UseEffect is useful for a bunch of things.
For handling native event handlers, like we just did there.
For handling other native APIs, like setTimeout, setInterval, and requestAnimationFrame.
HTTP calls either using the native APIs or using libraries and generally interacting with third party libraries that don't understand how React works.
But effects, in general, you should remember effects are an escape hatch from the React paradigm.
So effects are intended for dealing with things that don't understand React or that React doesn't understand.
They're not intended as a mechanism to watch your props and see if they change and React to them changing.
We don't need a mechanism for that because that's what React is.
That's why it's called React.
And if you're not sure what I'm talking about here, imagine we've added some sort functionality.
So we can sort our list by name or we can sort it by count.
And to write this code.
We might be tempted to write some code like this.
We might be tempted to add a bit of state that stores the sorted list of items.
And if we did that, then we'd also need to add an effect that watches the list of items and the sort by key and updates the list of sorted items whenever either of those changes.
But we shouldn't do this.
This is not how React is intended to work.
In fact, the docs say, don't use effects to transform data.
If you can calculate some information from the component's props, or its existing state variables during rendering, you should not put that information into that component's state.
So what should we do instead?
Here we can calculate the list of sorted items from the list of items plus the sort by key.
So we can just run that calculation each time the component renders.
This is the way that, this is the way.
All right, so useEffect, it stores state in an array inside a closure and it manages stuff that React doesn't understand.
Okay, so what have we learned today?
You'll be unsurprised, I'm sure, to learn that hooks store state in arrays inside closures.
useRef has no other special powers, that's all it does.
UseState triggers re renders as well, and useEffect is intended as an escape hatch.
If this is something that interests you, I would highly recommend having a look at Swyx's Getting Closure on React hooks.
He has both a blog post and a conference talk, and I'm super jealous that he thought of a way better name, for his talk than I did.
A couple of other things I found useful are Advanced React by Nadia Makarovic.
She also has blue hair and used to work at Atlassian, so she must be cool.
And Dan Abramovsky's JavaScript is really helpful for, building mental models for how to think about JavaScript.
And, If you also need a Zolder font for your project, this is Omni Jecala's Hylia Serif.
I don't know how to social media anymore, so that's what I look like on LinkedIn.
That's my Twitter handle down the bottom.
I'm not calling it that thing.
And the slides are at hooks.ez.codes.
Thank you.