React Hooks
React has come a long way since its beginning back in May 2013 when it was made open source. While the core concepts of the framework have remained the same, the implementation is always improving to help developers like yourself easily set up a SPA application. Features like State and Lifecycle Methods allow you to maintain data and route users around your website with ease with two types of components. These components are known as Class components (stateful), and functional components (stateless/display).
Sidenote: You should have an understanding of React
classes
,state
,life-cycle methods
, andContext
before learning hooks. While it's true that hooks will enhance and even replace some of these features, they still work the same and a base understanding of these concepts are essential. It is also helpful to be comfortable with es6destructuring
andliterals
as I will use them often on Objects and Arrays.
Hooks
Hooks were recently introduced to React to allow you to use things like state and lifecycle methods within a functional component. The hooks included are the following:
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
Wow that's quite the list right!? No worries, we will mostly be using the first 3 in the apps we make, so this article will focus on those three. However the others are just as useful and definitely deserve attention when you feel more comfortable with the concept. The Reactjs.org docs have documentation on all of them, which can be found here.
React Hooks API reference
useState
Use state is one of the most used hooks as it allows you to do exactly what it is called! You can now useState
inside of a functional component which means class
components are no longer necessary for you to have state in your application! What?! Yeah that's right, no more classes, no more class this
context, no more constructor
, super
, render
, or extends Component
.
Sidenote: There is not yet a React hook that let's us do the functionality of
componentDidCatch
, so for that error handler we will still use a class. The React team are planning on adding hooks eventually to manage this, so stay tuned on the React Docs.
We can do everything we've been doing with state within a function
component. So let's see this in action. First, here is a class component that maintains a count
in state and has a method that allows you to increment that value in state.
import React, { Component } from 'react'
class App extends Component {
constructor(){
super()
this.state = {
count: 0
}
}
increment = () => {
this.setState(prevState => ({
count: prevState.count + 1
}))
}
render(){
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.increment}>Increment</button>
</div>
)
}
}
export default App
Now here is that same functionality using the useState
hook in a function
component:
import React, { useState } from 'react'
const App = () => {
const [count, setCount] = useState(0)
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
export default App
That is quite a difference! Let's break this down and talk about the parts.
First we imported { useState }
from 'react'. All hooks are imported this way as a destructured part of the React
package.
Next we have this line:
const [count, setCount] = useState(0)
Let's break it down into it's parts:
useState(0)
- useState is a method that returns and array with two items inside.
- Whatever we put in the parenthesis is the initial state value, so here I am initializing the state at the number 0.
const [count, setCount] =
- Using ES6 array destructing, we declare state and setState variables. The first variable is the repsentation of our state, so
count
will be a variable currently pointing to the number 0. setCount
is the setter method given by useState, and can be seen as thesetState
method. We will use this method to update ourcount
state variable. It is convention to prefix the method name with the wordset
, followed by the part of state it will be setting.
- Using ES6 array destructing, we declare state and setState variables. The first variable is the repsentation of our state, so
Unlike the
state
in a class component, useState will take ANY datatype as it's initial state value! This means we no longer have to use an object to represent state if we don't need.
* Just like setState, all of the setter methods provided byuseState
allows for aprevState
callback that provides the prevState value of the state variable.
- Lastly we are using the count variable to display the count in the
<h1>
, and are using thesetCount
method to update out count in the button click.
Hook Rules
Before we get any further, we should quickly cover the rules around hooks. As cool as they are, they do have some restrictions that we need to be aware of.
Hooks can ONLY be used at the top level of our Component declaration, so you should not be declaring any variables with hooks inside of your return statement. This means you should NOT declare them within:
- loops(map)
- nested functions
- or as a part of a conditional statement.
If you break any of these rules, you will run into errors that crash your application.
Also we need to be aware that the set
method takes information to REPLACE the current state variable, so if our initial state
is something like an object or an array, we have to spread in previous state to maintain data as pieces of the data changes. For example, here is an array of string names that is our initial state, and a button that is adding a name to it.
import React, { useState } from 'react'
const ArrExample = () => {
const [names, setNames] = useState(["joe", "steve", "carol"])
return (
<div>
<div>
{ names.map((name, i) => <h1 key={i}>{name}</h1>) }
</div>
<button onClick={() => setNames(prev => [...prev, "frank"])}>Add Frank</button>
</div>
)
}
export default ArrExample
If you copy and look at this code in your web browser, you will see "joe", "steve", and "carol" listed on the DOM. When you click the button, setNames
spreads the existing names into the new array and add's "frank" to the end of the list. This is the exact same thing you would do with objects, so to see an example of this, let's look and our common handleChange
function we would write to handle <form> input changes.
import React, { useState } from 'react'
const Form = () => {
const [inputs, setInputs] = useState({title: "", description: ""})
const handleChange = e => {
const {name, value} = e.target
setInputs(prevInputs => ({
...prevInputs,
[name]: value
}))
}
return (
<form>
<input
type="text"
name="title"
value={inputs.title}
onChange={handleChange}/>
<input
type="text"
name="description"
value={inputs.description}
onChange={handleChange}/>
<button>Submit</button>
</form>
)
}
export default Form
The point of this example is to see what our setInputs
method needs to appropriately update the inputs. First we need to spread ...
in the existing inputs from prev
(previous State), and then overwrite the input the user is typing in by using the [e.target.name]: e.target.value
syntax.
Also notice that I wrote handleChange
as a nested function of our Form
component, this will be a normal pattern when you are using hooks as we no longer have a class
to write methods
in.
That is really it with useState
, functionally it's very simliar to state
and setState
in a class component, but allows us to break state up into different parts or manage them together.
More on the useState
hook can be found here.
useEffect
Now that we have covered state, what other peice from class
components do we need to account for if we are no longer using classes? The answer, lifecycle methods. How do we do our axios.get
request or window.addEventListener
in componentDidMount
if there is no class?
useEffect
is both componentDidMount
, componentDidUpdate
, and componentWillUnmount
all bundled into one. useEffect
is for performing side effects
on your component. Side effect include HTTP requests(data fetching), manual DOM manipulation(transitions on enter/exit/change), event listeners, and subscriptions.
HTTP requests
Depending on the side effect you are using, you may have to clean up the effect when the component unmounts. Let's first look at a side effect that doesn't need clean up, such as a componentDidMount HTTP request using axios. First, here is how we normally would have fetched data and saved it in state before hooks:
import React, { Component } from 'react'
import axios from 'axios'
class App extends Component {
constructor(){
super()
this.state = {
data: []
}
}
componentDidMount(){
axios.get('https://api.vschool.io/joeschmoe/todo')
.then(res => this.setState({ data: res.data }))
.catch(err => console.log(err))
}
render(){
return (
<div>
{ this.state.data.map(item => <h1 key={item._id}>{item.title}</h1>) }
</div>
)
}
}
export default App
Now, here is the same request being done with useEffect
and saving the data with useState
:
import React, { useEffect, useState } from 'react'
import axios from 'axios'
const App = () => {
const [data, setData] = useState([])
useEffect(() => {
axios.get('https://api.vschool.io/natej/todo')
.then(res => setData(res.data))
.catch(err => console.log(err))
}, [])
return (
<div>
{ data.map(item => <h1 key={item._id}>{item.title}</h1>) }
</div>
)
}
export default App
Let's break down this useEffect
into parts:
useEffect(() => {
- useEffect takes an anonymous function to fire
componentDidMount
and/orcomponentDidUpdate
.
- useEffect takes an anonymous function to fire
}, [])
:- This second argument
[]
is for telling theuseEffect
hook whether or not to fire again. A value could be placed here in the array thatuseEffect
will check and see if it has changed. If so, it will fire the hook again to re-update state (replacingcomponentDidUpdate
). Since we do not want this to run more than oncomponentDidMount
, we provide an empty array[]
so that there is never a change in values. If you remove this[]
from the above example,componentDidMount
will fire repeatedly causing our axios request to repeat itself indefinitely!
- This second argument
useEffect
by default will fire after everyrender
andupdate
that happens in our app, so the only way to tell it not to re-run is by using the[]
empty array or by placing a value for it to check to know whether to run again.
Events (and other side effects needing cleanup)
Great, so now let's look at useEffect
in the context of componentDidMount
& componentWillUnmount
when we need to use both to do some clean up on our component. In the following example, we will add a window event listener on componentDidMount
, and remove it on componentWillUnmount
using the useEffect
hook.
import React, { useEffect, useState } from 'react'
const App = () => {
const [color, setColor] = useState("blue")
useEffect(() => {
// componentDidMount
window.addEventListener('keydown', handleKeyDown)
return () => {
// componentWillUnmount
window.removeEventListener('keydown', handleKeyDown)
}
})
const handleKeyDown = e => {
if(e.which === 71){
setColor('green')
} else if(e.which === 82){
setColor('red')
} else {
setColor('blue')
}
}
return <div style={{backgroundColor: color, width: 200, height: 200}}></div>
}
export default App
This example sets up a window event listener for keydown
events, and changes the state
of our color
that affects the color of the displaying div
. To use componentWillUnmount
, you must return an anonymous function in your useEffect
hook as you can see in the above example: return () => {}
Other common side effects
that you would need componentWillUnmount
for are things like setInterval
, setTimeout
, and subscribing/unsubscribing from services.
More on the useEffect
hook can be read here.
useContext
Working in React comes with certain methods we have to repeat, such as writing HOC methods to make the <Consumer>
of a context reuseable for all components that need it. useContext
instead provides a way to consume your context values without having to use HOC's or extra component wrappers! (insert nerd celebration here). Let's see how it works. This first file is my Context file that provides both a CountProvider
wrapper for the value
, and a useCount
custom hook I can use to consume that Provider value
.
The pattern used below for useContext and other hooks was partly taken from Kent C. Dodds explanation on this topic. I definitely suggest checking out his work on this here: State Management
// CountProvider.js
import React, { useState, useContext, useMemo } from 'react'
// We first create our Context Variable
const CountContext = React.createContext()
// Then we can declare our Provider wrapper component
const CountProvider = props => {
const [count, setCount] = useState(0)
const value = useMemo(() => [count, setCount], [count])
return (
<CountContext.Provider value={value} {...props}/>
)
}
// Next we can declare our Consumer function by creating a custom hook!
const useCount = () => {
const context = useContext(CountContext)
if(!context){ // If unable to consume context, crash the program and let me know.
throw new Error("You must use CountProvider to consume Count Context")
}
return context
}
// We can then export both the Provider wrapper, and the custom hook for when need to consume the provider value.
export { CountProvider, useCount }
There are a few things going on in this file, starting with the CountProvider
component:
- First we use
useState
to create a count state variable and setter method. - Next we use
useMemo
to memoize this function.useMemo
is for performance optimization as it will remember the count value we put in the[count]
array brackets, and if it has encountered that update in the past it will re-use that computed value rather than re-compute and run the functions again.useMemo
is returning the same array[count, setCount]
and storing it in thevalue
variable. - Lastly we return the
CountContext.Provider
component with the value and props passed into it.
Next we have the useCount
custom hook: (this is replacing our typical HOC function which would have called withCount
:
- First we declared the function with the convention of
use
to name our functionuseCount
. - Next, we use
useContext
to grab the context variable from the top of the page. WhenuseContext
is given a context variable, it returns to you thevalue
object from the Provider, so no HOC's or<Consumer>
components have to be explicitely used. We're letting React do that for you. - Lastly, we first check to make sure we were able to get the context and throw and error if we didn't. If it works, we return the context value object.
Now let's talk about some big differences. We are exporting two functions from this file, CountProvider
and useCount
. <CountProvider/>
can still be wrapped around the <App/>
component in our index.js
, or it can be wrapped around the section of JSX that needs access to it's value
object. In this case, I will just wrap it around the App
component.
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.js'
import {CountProvider} from './context/CountProvider.js'
ReactDOM.render(
<CountProvider>
<App />
</CountProvider>,
document.getElementById('root'))
It is important to know that the Provider component does NOT have to be wrapped this high up in our Component tree if only a sub-section of our application needs the ability to consume. So you could import the
CountProvider
component a couple levels deep and wrap your JSX that needs to be able to consume.
Then in our App.js, we can import the useCount
custom hook to get access to the CountProvider
's value object.
// App.js
import React from 'react'
import { useCount } from './context/CountProvider.js'
const App = () => {
const {count, setCount} = useCount()
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}> Increment </button>
<button onClick={() => setCount(count - 1)}> Decrement </button>
</div>
)
}
export default App
As we can see, our self-made useCount()
hook returns the variable and the setter method from our Context, the only difference is we do not have to access props
to get this information like you do when you wrap the export in a HOC function.
Congrats! You have seen the 3 main hooks (plus useMemo
) React just introduced and can begin to use them in your own code. They are safe to use as they are fully integrated into React as of the most recent update (late 2018 - early 2019). As you become more comfortable you should definitely check out the other hooks they are providing. Here is a summary of what they do:
-
useReducer:
- Have you ever used
redux
? This is for you. You write areducer
function as you would with redux and provide it touseReducer(myReducer)
. In return, it gives you thestore
and thedispatch
method!
- Have you ever used
-
useCallback
- Uses
memoization
to cache and optimize a specific callback function.
- Uses
-
useMemo
- Also uses
memoization
to cache expensive create functions and only calls it again if the values used in the create function have changed.
- Also uses
-
useRef
- Used to assign a React
ref
to a variable without the ref callback.
- Used to assign a React
-
useImperativeHandle
- A way to customize the instance value of a ref (not typically used, but availabled if needed when forwarding Refs).
-
useLayoutEffect
- Similar to
useEffect
but it firessynchronously
after all DOM mutations. This is whyuseEffect
should be used for layout effects (animations, transitions) when possible.
- Similar to
-
useDebugValue
- When you make your own custom hooks that use React hooks, this allows you to label your custom hook in dev-tools.
Lastly, the only hooks that don't exist yet are for componentDidCatch
in Error Boundaries
, and getDerivedStateFromError
. This means you would still need to use the React.Component
in a class
to use those features until further updates bring us more hooks!