A Beginner’s Guide to React, Part 2: Hooks Edition
--
If you’re a React engineer and you haven’t been living on the moon for the past nine months, chances are you’ve heard about React Hooks. The TL;DR is that before Hooks, class components were the only components that could track state
and make requests to an API. Hooks are functions that enable us to use state
and other React features in functional components. In this post, I’m going to walk you through four of the major Hooks — useState
, useEffect
, useContext
, and useReducer
— , all while refactoring the PhotoFinder app I built to go along with my 2018 post, “A Beginner’s Guide to React.” Let’s get started.
useState()
Before Hooks, the concept of state
drew the clearest line in the sand between class components and functional components: class components had it, and functional components didn’t. Now, if you want your functional component to have state
, you simply import the useState
function from React and then…use it!
Here’s an example of a simple component that enables the user to select a region and a language:
Here it is in action:
In this example component, we simply call useState
and pass it a value (which can be of any data type) for the initial state
. Since we have two pieces of state — region
and language
— , we call useState
twice. Next, we use the array de-structuring syntax to pull off the value in state
we want to create, as well as the setter function for this — and only this — specific piece of state
. The naming convention for this setter function is always set{yourPieceOfState}
. We are now able to simply use the variable representing each piece of state
anywhere in our component, and when we want to change that value in state
, we call the setter function and pass it a new value.
I mentioned before that useState
can take any data type as an argument. This might seem strange at first, since we’re used to seeing state
as an object. As such, it might seem more natural to pass an object to useState
. This can be done, but there’s one important caveat to keep in mind. Before I explain, take a look at this refactored region and language selector component, which passes an object to useState
:
The crucial takeaway from this refactor is on lines 6–9 and 15–18 — namely, when you pass an object to useState
and then want to update only part of it, the old state
will not be merged together with the new state
in the way the setState
method behaves in class components. As such, if we initialize state
with an object and then want to update only part of it, we must manually merge state
by spreading the values that aren’t changing into the setter function, along with the value that is changing. Ye be warned!
We’ll need one more Hook before we can start refactoring the PhotoFinder app from “A Beginner’s Guide to React,” so let’s move on.
useEffect()
In traditional (read: pre-Hooks) React, all data fetching and subscription management had to be handled within component lifecycle methods, which are only available to class components. But the useEffect
Hook, which takes a function as an argument, enables us to interact with the outside world from within a functional component. I’m going to show you how it works, but first, let’s get up to speed in our refactor of the PhotoFinder app.
Do you remember how, in the initial version of our app, we held values for the user’s search term
and the returned photos
in state
? When the app first loaded and the user hadn’t typed anything, we fetched photos inside componentDidMount
that matched the search term coding
and saved those photos to state
. Then, whenever the user typed into the search bar, we fetched photos again — this time, matching the new term
— and saved those photos to state
. Let’s look at how we can get the same functionality with a combination of useState
and useEffect
.
First, we need to call the useState
Hook two times — first for the term
, and again for the array of photos
. In order to match the hook-less version of our app, we’ll pass the string “coding” as the initial term
state
, and an empty array for the initial photos
state
.
Next, it’s time to set up our useEffect
Hook. As I mentioned, useEffect
takes a function as an argument. In this case, the body of that function will call fetchPhotos
, which calls the Unsplash API and sets the resulting photos in state
using the setPhotos
setter method, which we got from our call to useState
.
You may be wondering about the second argument we’ve passed to useEffect
— namely, the array containing term
. This second argument is important because useEffect
, by default, is called after every render; the only way you can control when it is called is by passing it an array as a second argument. If that array is empty, useEffect
will only be called twice: once when the component mounts and once when the component unmounts. But if the array isn’t empty — say, if it includes a value from state
— useEffect
will only be called when that particular value changes. In our case, useEffect
should only be called if the value of term
in state changes, so we include term
inside that array. If we fail to pass useEffect
a second argument at all, we’ll get caught in an infinite loop — calling the API, setting state
, an re-rendering, ad nauseam.
The useEffect
Hook also enables us to set up — and clean up — event listeners. For instance, you might set up an event listener in the body of the function, and then clean it up in the function that useEffect
optionally returns (useEffect
returns a function or nothing). If it does, in fact, return a function, that function will be called right before the component unmounts, thereby acting as the “clean up” function.
The main takeaway here is that the useEffect
Hook represents the union of componentDidMount
, componentDidUpdate
, and componentWillUnmount
— but with greater flexibility and power. The traditional lifecycle methods forced us to smush unrelated tasks together (or break related tasks apart) based on when in the component’s lifecycle they should occur. But with useEffect
, which, like useState
, can be called multiple times, we’re able to group related functions, API calls, and values together, all while controlling mounting and unmounting behavior. Pretty cool, huh?
useContext()
If you’re a React developer, you’re probably all-too-familiar with the concept of props drilling — that is, the process of passing data down through a component tree, sometimes going several levels deep. You might find yourself in a deeply nested component when you realize you’re getting a value from props
and you don’t know what it is or where it’s coming from. You’re then forced to embark on a mission to retrace your steps and figure out the source of the mysterious value. Sometimes, components in the middle of the tree don’t need a certain props
value at all , but they have to receive it in order to pass it further down. It can get pretty treacherous.
The useContext
Hook offers a new alternative, enabling us to share data anywhere in the component tree without having to manually pass it down via props
— and without having to rely on the traditional React Context API, which can lead to deeply nested Consumers. As with the React Context API, you still need to call React.createContext()
to initialize your piece of global state (i.e. context
), and you still wrap your App
in a Provider
with the value pointing to a particular piece of global state. But instead of having to wrap the component that needs context
in a Consumer
, all you have to do is call useContext
at the top of your component and deconstruct the value you need off of its return value.
If it’s confusing now, don’t worry — I’m going to give an example in a moment. But first, we need to go over the useReducer
Hook.
useReducer()
The useReducer
Hook goes hand-in-hand with useContext
, as it enables you to manage global state (i.e. context
) — without Redux — from inside any component. Let’s walk through the steps of using useContext
and useReducer
in tandem. I’m going to use these two Hooks to enable users to “favorite” particular photos in our PhotoFinder app, but please note: it’s best to only use context
for pieces of state
that your entire app depends on, such as a user or region data. In our case, it wouldn’t be very complicated to just hold the user’s favorite photos in state
, especially because our components are not deeply nested (and in an ideal world, our app would have a back end, which would enable us to save the user’s favorite photos in a database). Please keep in mind that this example is intended simply as a teaching tool. That being said — let’s proceed.
First, Create a file called context.js
, where you will call React.createContext
to create your initial context
object. Don’t forget to export it!
Then, create a file called reducer.js
, where you will write a reducer
function that takes state
and action
as arguments (think: Redux). This function will essentially be a giant switch
statement, switching on the type
of action
that is passed in. In our case, all we’re going to want to do is enable a user to favorite and unfavorite photos, which we will do with a single action
type
: TOGGLE_FAVORITE
.
After we have our context
and reducer
files set up, we call useContext
at the top level of our App
component to create a piece of initial global state
. On the next line, we call useReducer
and pass, as the first argument, our reducer
function, and as the second argument, the piece of initial global state created by useContext
(again — don’t forget your imports!). Using array de-structuring syntax, we’re able to grab hold of global state
and a dispatch
function from the useReducer
call. Our dispatch
function is what’s going to enable us to talk to the reducer
function we created in reducer.js
.
Next, it’s time to wrap our entire App
in your context Provider
, passing both state
and dispatch
(which we got from useReducer
) into the value
prop in order to gain access to it in whatever component we call useContext
. Our App
component should look like this:
If we were holding favorites
in state
, rather than in context
, we’d have to pass favorites
into the PhotoList
component, which would in turn pass the relevant data (namely, is a particular photo a favorite or not?) into each PhotoListItem
. PhotoList
doesn’t actually need to know about the user’s favorites
, so useEffect
will enable us to skip PhotoList
entirely. We simply import and call useContext
at the top of the PhotoListItem
component, which gives us access to state
(we de-structure favorites
off of it) and dispatch
. No Consumer
necessary!
Now, we need to add a “heart” icon to each PhotoListItem
; if that photo is included in the user’s favorites
, the icon is red; otherwise, it’s grey. When the user clicks that icon, the dispatch
function is called with the action type
TOGGLE_FAVORITE
and a payload of imageObj
, which we create at the top of the component and which includes the image’s url
and id
. Our PhotoListItem
should look like this:
The dispatch
call, which occurs when the icon is clicked, sends the action
to the reducer
function inside of reducer.js
. The logic in the reducer
function is outside the scope of this post, but suffice it to say that it looks through the user’s favorites
in global state
, and if the particular photo in the payload
is in that array, it removes it. If it’s not in the favorites
array, it adds it. The function then returns a new value for state
, which triggers a re-render.
Ta-da!
Conclusion
I’ve called this post “A Beginner’s Guide to React, Part 2” because it builds off the material in a post I wrote in 2018, before the introduction of Hooks. The truth, though, is that Hooks are not really within the realm of “beginner’s React”; they’re still very new, and they take some time to get used to — even for seasoned React developers. If you’re confused — especially by useContext
and useReducer
— don’t fret! Start by practicing with useState
and useContext
, and work your way up to more complex Hooks. I highly recommend Reed Barger’s Udemy course on hooks if you’re looking for more guidance.
If you’d like to spend more time poking around in the code for this project, I’ve added the repo to my GitHub. And if you’d like to see the original, hook-less version of the project, it’s still on GitHub, as well.
Now, go forth and hook!