About Me
Blog

Intro To React: Part 2

How do I use__? A guide to React hooks

December 16, 2020

11 min read

Last updated: July 27, 2021


Prefer this in video form? I ran a React workshop for Hackers at Cambridge last month that covered React hooks in the context of a web camera app.

Before React Hooks

Before we discuss React hooks, it’s worth briefly highlighting the problem they solve.

See, in the last post, we talked about React components as pure functions - they take in props and return a rendered output, and re-render the component every time the props changed.

What if we want to store state between renders (e.g. the number of times a button was clicked)?

You can’t hold state in a function in JavaScript, so instead we use classes. We access state using this.state and props using this.props. We can set the initial state in the constructor method, and update the state between renders using this.setState.

All our rendering logic is in the render method.

class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0,
otherValue: "Init value",
}
}
render() {
return (
<div>
<h1> {this.props.heading} </h1>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 })
}}
>
{this.state.count}
</button>
</div>
)
}
}

Lifecycle Methods

See now we have classes, we can add other methods to our class, not just constructor and render. So React exposed some lifecyle methods - these were executed when the component was at that stage of its lifecycle. So you could use these to set-up, update and clean-up your state as your component was being used.

For example, componentDidMount is called the first time the component is rendered (and thus “mounted” to the DOM), componentDidUpdate is called after a component re-rendered, and componentWillUnmount is called just before a component was being removed from the DOM (if no longer being rendered and shown to the user).

There’s a whole host of other lifecycle methods, shouldComponentUpdate, getSnapshotBeforeUpdate etc. Confused much? I sure am.

It’s one thing to figure out which order these lifecycle methods execute, but it’s another thing to track how they affect state, since updates to state are handled asynchronously. React only makes sure the updates are done by the next update, but there’s no guarantee the next lifecycle method will see the updated state of the earlier lifecycle method.

class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0,
otherValue: "Init value",
}
}
componentDidMount(){
...
}
componentDidUpdate(){
...
}
componentWillUnmount(){
...
}
render() {
return (
<div>
<h1> {this.props.heading} </h1>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 })
}}
>
{this.state.count}
</button>
</div>
)
}
}

Enter React Hooks

Fragile. Hard to reason about. State spread over a bunch of methods. If you remember from last time, this was the situation we had with the actual DOM!

What did React do? It abstracted over the real DOM with the virtual DOM. And React Hooks are the same - they abstract over this messiness - we let React do the work for us.

No more messy class components, but instead, we have nice clean function components.

If you notice now, we’re referring to props instead of this.props, count rather than this.state.count and setCount rather than this.setState({count: _}). Much cleaner! And this is before you see how we’re going to clean up lifecycle methods.

Let’s talk about Hooks get us there.

const MyComponent = (props) => {
// Hooks will let us clean up state and lifecycle methods
this.state = {
count: 0,
otherValue: "Init value",
}
componentDidMount(){
...
}
componentDidUpdate(){
...
}
componentWillUnmount(){
...
}
return (
<div>
<h1> {props.heading} </h1>
<button
onClick={() => {
setCount(count + 1)
}}
>
{count}
</button>
</div>
)
}

Using Hooks

React Hooks are the special functions provided by React. We’ll be looking at 6 of them in this post:

  • useState
  • useEffect
  • useCallBack
  • useRef
  • useReducer
  • useContext

We import Hooks from the React library. For example, importing useState, and useEffect:

import React, {useState, useEffect} from "react

We can import other hooks in a similar manner. e.g. importing useReducer:

import React, {useReducer} from "react

Hook #1: useState

If we strip back our class component, there’s really three things we want to do with state:

  1. Set the initial state
  2. Get the current value of the state
  3. Be able to update the state.

The useState hook lets us do just that and offloads the boilerplate to React. We pass in the initial value of the state to useState, and React returns the current value, and a special setValue function that lets us update the state. Just like how React re-renders the component every time the props change, React will re-render every time state changes.

let [value, setValue] = useState(initValue)

It’s important to remember that, like any old function, we can set the names of the returned values to whatever we want. And we can call it as many times as we’d like, and we get a fresh value each time.

let [count, setCount] = useState(0)
let [isAmazing, setIsAmazing] = useState(true)

So the first call initialises a count state variable to 0, and the other initialises isAmazing to true. Note here that it’s convention that if we call our value foo, then the update function should be called setFoo.

Again, there’s nothing special about this (apart from the name), you can use it like you’d use any other function or variable. Here, we’ve used a count variable to track the number of times we clicked the button, incrementing the count every time it was clicked.

const IncButton = () => {
let [count, setCount] = useState(0)
return (
<button
onClick={() => {
setCount(count + 1)
}}
>
{"This button was clicked " + count + " times"}
</button>
)
}

Data flows downwards

We follow normal JS scoping rules, so the count variable can’t be accessed outside the component it is defined in. The only way to pass state is as props to another component. Likewise, the only way allow another component to update the state is if you pass the special set__ function to it via its props.

We say data flows downwards from the parent to the children. Here this means that Child1 can read and update the state, Child2 can only read the state, but Main cannot read the state.

const ParentComponent = () => {
const [someValue, setSomeValue] = useState({})
return (
<div>
<Child1 val={someValue} setVal={setSomeValue} />
<Child2 val={someValue} />
</div>
)
}
const Main = () => {
// can't access someValue here!
return <ParentComponent />
}

A bit about Hooks under the hood

Let’s go back to our example:

let [count, setCount] = useState(0)
let [isAmazing, setIsAmazing] = useState(true)

How does React know which call to useState() is which? We’re the ones that give the returned values names, so it can’t use names to distinguish between them. There’s nothing to stop us from swapping their names!

// swapped names!
let [isAmazing, setIsAmazing] = useState(0)
let [count, setCount] = useState(true)

React tracks each call by considering the order useState was called. Intuitively, it’ll call the first state useState1, the second state useState2 etc.

For React to do this, the order and number of calls to useState must be the same every render. So we can’t call useState inside a block that is conditionally executed:

let [count, setCount] = useState(true)
if (someCondition) {
let [isAmazing, setIsAmazing] = useState(0)
}
...

Do we call useState once or twice in this component? That depends on someCondition. React can’t tell whether someCondition will evaluate to true or false, so can’t tell how many times it’s called. Therefore, this code isn’t allowed.

How about:

let [count, setCount] = useState(true)
if (someCondition) {
return <div> {count} </div>
}
let [isAmazing, setIsAmazing] = useState(0)
return ...

We’re not calling useState in a conditional block here. But React still doesn’t know how many times useState is called. If we return the <div> {count} </div>, then we only call useState once, but if we reach the second return statement, then useState is called twice. React doesn’t know which one we’ll render, because it doesn’t know what someCondition is. So we’re not allowed to write this either.

This “hooks can’t be called conditionally” rule is true for all hooks, not just useState. So when you get the error React hook called conditionally you’ll know why!

A rule of thumb I like to use: declare your hooks upfront, right at the very start of the body of your component!

With that out of the way, let’s look at some more React hooks.

Hook #2: useEffect

Remember those lifecycle methods? They’re useful for logic that you want to fire off that isn’t related to the render, e.g. if you want to make a POST request to an API, or write to your database and so on. You might initially connect to the database in your componentDidMount function, and then update the database in componentDidUpdate and so on.

Like we discussed, those lifecycle methods are messy. All we care about is that this code is asynchronous (like a database request) and shouldn’t block the rendering of the component.

So we’ll let React handle this lifecycle stuff for us. All we’ll tell it is what code to execute, and optionally (by returning a callback function) tell it how to clean-up after the component is done (e.g. shut down the database connection).

This hook is called: useEffect (it lets us use “side effects”).

useEffect(() => {
// code to be executed asynchronously
const database = new Database()
database.makeDBRequest()
return () => {
database.cleanUpDBConnection()
} // callback to clean-up code
})

useEffect effectively splits our code into synchronous code used for rendering (outside useEffect), and asynchronous code for side-effects (within useEffect).

The thing with asynchronous code is we don’t know when it’ll execute, so don’t write code that expects effects to execute in a certain order. Below, the second effect might execute before the first one for all we know:

useEffect(() => {
...
})
useEffect(() => {
...
})

So when is useEffect run?

useEffect is run the first time a component is rendered, and run again every time the component is re-rendered. React re-runs the effect so the contents of the useEffect body always have the latest values of props and state, as in the example below.

useEffect(() => {
database.updateDB(props.dbValue)
})

Maybe you don’t want to fire off the effect every time the component re-renders.

For example, if props.someOtherValue changes, this will cause React to re-render the component as props have changed. However database queries are expensive, so you don’t want to make a request to the database if props.dbValue hasn’t changed.

You can specify which values should cause a re-render by passing in a dependency array as a second argument to useEffect. React will only re-render when any of the dependencies change.

Passing an empty dependency array i.e. useEffect( () => {...}, []) means that useEffect will fire only once, on the initial render.

We want to update the database every time props.dbValue changes, so we add it to the dependencies array:

useEffect(() => {
database.updateDB(props.dbValue)
}, [props.dbValue])

Perfect? One hitch. What happens if database changes? We haven’t told React about database as a dependency, so React won’t fire off useEffect with the latest value. Bugggggg.

See, dependencies don’t just include props or state. They mean anything declared in the component outside the useEffect body.

I’d strongly advise against picking and choosing dependencies as this will lead to bugs with stale values. This is such a common mistake, React provides a check-exhaustive-deps linter rule to catch this.

Got a dependency you haven’t declared in the dependency array? There are three ways to address this:

  • move the dependency outside the component (if it doesn’t depend on props or state it shouldn’t be in the component!)
  • move the dependency inside the body of useEffect
  • add the dependency to the dependency array
// WRONG
const Component = (props) =>{
const database = new Database()
useEffect(() => {
database.updateDB(props.dbValue)
}, [props.dbValue])
}
// RIGHT (move outside component)
const database = new Database()
const Component = (props) =>{
useEffect(() => {
database.updateDB(props.dbValue)
}, [props.dbValue])
...
}
// RIGHT (move into effect)
const Component = (props) =>{
useEffect(() => {
const database = new Database()
database.updateDB(props.dbValue)
}, [props.dbValue])
}
// RIGHT (add to deps array)
const Component = (props) =>{
const database = new Database()
useEffect(() => {
database.updateDB(props.dbValue)
}, [props.dbValue, database])
}

When I mean anything declared in a component, I mean functions too:

// WRONG
const Component = (props) =>{
const doSomething = (val) => {...};
useEffect(() => {
doSomething(props.val)
}, [props.dbValue])
}
// RIGHT (move outside component)
const doSomething = (val) => {...};
const Component = (props) =>{
useEffect(() => {
doSomething(props.val)
}, [props.dbValue])
}
// RIGHT (move into effect)
const Component = (props) =>{
useEffect(() => {
const doSomething = (val) => {...};
doSomething(props.val)
}, [props.val])
}
// RIGHT (add to deps array)
const doSomething = (val) => {...};
useEffect(() => {
doSomething(props.val)
}, [props.val, doSomething])

There’s a catch with adding functions to a dependency array though. Functions are recomputed every time the component is re-rendered, for the same reason that its function body should not contain stale values.

So actually here we don’t have a single doSomething function across renders, but a fresh function doSomething1 doSomething2 etc. for each re-render.

useEffect therefore fires on every re-render, because each render its dependency changes from doSomething1 to doSomething2 and so on.

But what if we could tell React to only re-compute a function when it’s actually changed?

Hook #3: useCallback

useCallback is like useEffect but for functions - we wrap the function in a useCallback and specify the dependency array (as before, remember all dependencies!). React then caches the function instance across renders and uses the dependency array to determine when to create a fresh instance:

// BEFORE (run every render)
const doSomething = val => {
return props.otherVal + val
}
// AFTER (run when props.otherVal changes)
const doSomething = useCallback(
val => {
return props.otherVal + val
},
[props.otherVal]
)

By not recomputing functions every render, it seems you’re getting performance gains, so surely you should wrap every function in a component with useCallback? Not quite. It’s actually usually cheaper for React to create a new function than for it to monitor a function to determine when it should change.

Rule of thumb: Only use useCallback for a function if

  • the function instance is expensive to recompute.
  • the function is a useEffect hook’s dependency

There’s a related hook useMemo which executes a function and caches its result, rather than caching the function instance itself (as in the case of useCallback).

Hook #4: useRef

So far with our tour of React everything has been immutable. We don’t update the function across re-renders, we throw it away and generate a fresh one. But what if you really want an escape hatch to write mutable code?

useRef lets you do just that. You pass in the initial value and React returns you a “box” - an object with one field: current. You can update the contents of this object by directly mutating the current field (there’s no special setCurrent function required).

let someRef = useRef(initVal);
...
someRef.current = newVal;

Refs are particularly useful if you want to get a reference to a particular DOM element. If you set it to the ref attribute of a given element, React will automatically update the .current value to point to that element.

const Child = props => <input> {props.inputName} </input>
const Parent = () => {
let childRef = useRef(null)
return (
<div>
...
<Child ref={childRef} inputName="Form Input" />
</div>
)
}

In this case, childRef.current will be set to the Child component. Note ref is a special attribute used by React, not a property of the component Child, so we can’t access props.ref like we can access props.inputName.

If we want the ref to be accessed by the child component like another prop, we can use React.forwardRef when defining our child component:

const Child = React.forwardRef((props, ref) => <input ref={ref}> {props.inputName} </input>
const Parent = () => {
let childRef = useRef(null)
return (
<div>
...
<Child ref={childRef} inputName="Form Input" />
</div>
)
}

Now Child forwards the ref to the <input> DOM element, so childRef.current is set to that <input> DOM element. This means our Parent component can now set attributes of the <input> DOM. Use at your peril!

Managing State in Larger React Apps

So far, state management in React has mainly been through useState and passing state downwards using props. However, as your apps get larger, you’ll need more advanced ways of managing state.

We’ll look at the following problems:

  • state spread all over a large component
  • state used by a lot of components.

The first problem can be solved using reducers, and the second by using context. By now, it won’t surprise you to know that the corresponding hooks associated with these patterns are useReducer and useContext.

Reducers

As the state of your component increases, you might end up with lots of calls to useState.

let [isCameraOn, setIsCameraOn] = useState(false);
let [isFrontCamera, setIsFrontCamera] = useState(true);
let [cameraImage, setCameraImage] = useState(null);
let [cameraResolution, setCameraResolution] = useState("5MP");
...
<Button onClick={ () => {
isFrontCamera ? setCameraResolution("16MP"): setCameraResolution("5MP");
setIsFrontCamera(!isFrontCamera);
}}/>
...
<Button onClick={image => {
if (isCameraOn){
setCameraImage(image);
} else {
...
}
})/>

Our updates to state are complex and are spread all over the component. How do we clean up this code so it’s easier to reason about?

Now, the traditional approach might be to define functions for each:

function flipCamera(){
isFrontCamera ? setCameraResolution("16MP"): setCameraResolution("5MP");
setIsFrontCamera(!isFrontCamera);
}
function takePhoto(image){
if (isCameraOn){
setCameraImage(image);
} else {
...
}
}
...
<Button onClick={flipCamera}/>
...
<Button onClick={takePhoto}/>

But these useState invocations and function calls all represent a single entity: camera state. React offers a design pattern that groups together related state and updates: a reducer design pattern.

We group our camera state into one object.

let initCameraState = {
isCameraOn: false,
isFrontCamera: true,
cameraImage: null,
cameraResolution: "5MP",
}

Likewise we group all our update functions’ logic into a single reducer function. The reducer takes in two arguments: the current state, and an action, and returns the updated state. This second action argument is an object that contains additional information about the update. We can use its type field to specify which update function to apply. We can also pass in additional arguments as additional fields in the action object.

For example, our takePhoto function needs an image as argument, so we would supply this in the action object and call action.image:

function cameraReducer(state, action){
switch(action.type){
case "flip_camera":
// the body of the flipCamera function
let cameraResolution = isFrontCamera ? "16MP": "5MP";
return {...state, cameraResolution, isFrontCamera: !state.isFrontCamera}
case "take_photo":
// the body of the takePhoto function
if (isCameraOn){
return {...state, cameraImage: action.image}
} else {
...
}
}
}

It’s common practice to write out the types of actions as fields of an ACTIONS object (the JS workaround for an enum) rather than strings that can be misspelled.

const ACTIONS = {
FLIP_CAMERA: "flip_camera",
TAKE_PHOTO: "take_photo"
}
function cameraReducer(state, action){
switch(action.type){
case ACTIONS.FLIP_CAMERA:
// the body of the flipCamera function
let cameraResolution = isFrontCamera ? "16MP": "5MP";
return {...state, cameraResolution, isFrontCamera: !state.isFrontCamera}
case ACTIONS.TAKE_PHOTO:
// the body of the takePhoto function
if (isCameraOn){
return {...state, cameraImage: action.image}
} else {
...
}
}
}

Hook #5 useReducer

Now, we have our initial state and a reducer function that tells React how to update the state, we can use a useReducer hook in place of useState. This looks very similar, but we pass in an additional cameraReducer argument, and instead of setCameraState, we have a dispatch function:

let [cameraState, dispatch] = useReducer(cameraReducer, initCameraState)

This dispatch function takes in an action and updates the state for us (it’s just like setState but at the level of “actions”).

// to flip the camera, we dispatch an action of that type
<Button onClick={() => { dispatch({type: ACTIONS.FLIP_CAMERA})} }/>
...
<Button onClick={image => { dispatch({type: ACTIONS.TAKE_PHOTO, image})}}/>

React Context

Now let’s look at another scenario.

What if we have state that we want to access across lots of components like a theme? We define it in the top-level component and pass it as props to each of the children.

const Child1 = props => {
return <div> Child 1's theme is: " + {props.theme} </div>
}
const Child2 = props => {
return <div> Child 2's theme is: " + {props.theme} </div>
}
const App = () => {
const theme = "dark"
return (
<div>
<Child1 theme={theme} />
<Child2 theme={theme} />
</div>
)
}

This is a bit cumbersome though, as we need to add theme to all props. What if we could just access some shared state directly?

Spoiler. We can. We call this shared state between components context.

We can create context, by calling React.createContext, passing in a default value for the context:

const Theme = React.createContext("light")

This returned Theme object is has two fields: a Provider component which sets the value, and a Consumer component.

The Provider component sets (provides) the value of the context for all its children, and the Consumer component lets you read (consume) that context.

So we can rewrite the earlier example to use React Context instead. The Theme.Provider component takes a value prop to set the context, and the Theme.Consumer component takes as its child a function theme => (component). We call this theme argument a render prop.

const Theme = React.createContext("light")
const Child1 = () => {
return (
<Theme.Consumer>
{theme => <div> Child 1's theme is: " + {theme} </div>}
</Theme.Consumer>
)
}
const Child2 = () => {
return (
<Theme.Consumer>
{theme => <div> Child 2's theme is: " + {theme} </div>}
</Theme.Consumer>
)
}
const App = () => {
const theme = "dark"
return (
<div>
<Theme.Provider value={theme}>
<Child1 />
<Child2 />
</Theme.Provider>
</div>
)
}

The Consumer reads the value of the closest parent Provider component (or the default if there’s no parent Provider) - this allows you to override the context in children components. The comments indicate what value for the theme context a consumer would read:

const App = () =>(
<Theme.Provider value={theme1}>
// context = theme1
</Theme.Provider>
// context = light (default value)
<Theme.Provider value={theme2}>
// context = theme2
<Theme.Provider value={theme3}>
// context = theme3
</Theme.Provider>
</Theme.Provider>
)

Hook #6 useContext

React Context as an API pre-dates Hooks. But, as we’ve seen with useState and useEffect, the API before Hooks was messy, and Hooks came to clean it up.

The offending piece of syntax is the render prop we have to use for Consumer components. This becomes a nested mess the more pieces of context we try to consume:

const ThemeContext = React.createContext(defaultTheme);
const LocationContext = React.createContext(defaultLocation);
const UserContext = React.createContext(defaultUser);
const SomeComponent = () => {
return (
<ThemeContext.Consumer>
{theme => {
if (theme== "blah")
return <div> Not supported </div>
return (
<div>
...
<UserContext.Consumer>
{user => (
....
<LocationContext.Consumer>
{location => (...)}
</LocationContext.Consumer>
)}
</UserContext.Consumer>
</div>
)}
}
</ThemeContext.Consumer>);
}

It’s not clear when we’re consuming and when we’re rendering. And the render props means we have functions and logic nested in our render output. We want to disentangle the context from the rendered output.

Enter: useContext. We pass in the context we care about as an argument, and it returns the value. Now we can pull the logic out to the top level:

const SomeComponent = () => {
const theme = useContext(ThemeContext)
const user = useContext(UserContext)
const location = useContext(LocationContext)
if (theme == "blah") {
return <div> Not supported </div>
}
return <div>...</div>
}

Much cleaner don’t you think?

Define your own custom hooks

The fun doesn’t stop with the hooks provided by React. Hooks are just functions after all, and you can compose them to build more complex custom hooks. These custom hooks can take any arguments, and return anything.

The only convention is that you prefix these functions with use__ so React’s linter can pick it up and apply the Hook’s ordering and no-conditional-hooks rules.

For example, suppose we had this logic to classify an image:

let [imageToClassify, setImageToClassify] = useState(initImage)
const [class, setClass] = useState("unclassified")
useEffect(() => {
// make backend request
const response = classifier.classifyImg(imageToClassify)
setClass(response.class)
})

We want to use reuse this logic to classify any image. How might we write this as a custom hook?

Well, if you look at this section of code, it needs an initImage to be defined, so we’ll pass that in as an argument to initialise our classifier.

What should this return? Naturally, the class of the image. It also needs to return the setImageToClassify function, so we can set new images to classify.

So our custom hook looks like:

function useClassifyImage(initImage) {
let [imageToClassify, setImageToClassify] = useState(initImage)
const [class, setClass] = useState("unclassified")
useEffect(() => {
// make backend request
const response = classifier.classifyImg(imageToClassify)
setClass(response.class)
})
return [result, setImageToClassify]
}

Let’s go ahead and call it in a Classifier component!

const Classifier = () => {
let [class, classifyImage] = useClassifyImage(null)
return (
<div>
<Camera onCapture={classifyImage}>
<div> {"The class is: " + class} </div>
</div>
)
}

Wrap Up

We’ve covered a whole lot of content there, but this should set you up in good stead to use Hooks in your own projects.

If you spot other hooks defined in other libraries that build on React, know that there’s nothing to worry about - under the hood they defined their custom hooks just like we did.

Share This On Twitter

If you liked this post, please consider sharing it with your network. If you have any questions, tweet away and I’ll answer :) I also tweet when new posts drop!

PS: I also share helpful tips and links as I'm learning - so you get them well before they make their way into a post!

Mukul Rathi
© Mukul Rathi 2023