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><buttononClick={() => {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><buttononClick={() => {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 methodsthis.state = {count: 0,otherValue: "Init value",}componentDidMount(){...}componentDidUpdate(){...}componentWillUnmount(){...}return (<div><h1> {props.heading} </h1><buttononClick={() => {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:
- Set the initial state
- Get the current value of the state
- 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 (<buttononClick={() => {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 asynchronouslyconst 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
// WRONGconst 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:
// WRONGconst 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 functionlet cameraResolution = isFrontCamera ? "16MP": "5MP";return {...state, cameraResolution, isFrontCamera: !state.isFrontCamera}case "take_photo":// the body of the takePhoto functionif (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 functionlet cameraResolution = isFrontCamera ? "16MP": "5MP";return {...state, cameraResolution, isFrontCamera: !state.isFrontCamera}case ACTIONS.TAKE_PHOTO:// the body of the takePhoto functionif (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 requestconst 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 requestconst 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.