Class components are ugly and hard to understand, but were needed as it was the only way to access state
. Hooks alleviate this, as they are functions that we can call to hook into state
and various lifecycle methods. By using hooks, we can introduce state
into function components, eliminating the need for always using class components. Instead of setting state in a constructor()
using this.state = {name: "inital"}
, we can instead call the React function useState
with the syntax const [name, setName] = React.useState("initial")
. The first value we assign is the state we want to track, the second value is a function to alter only that piece of state, replacing the need for this.setState
calls.
Since we no longer extend
a React.Component
, we use another hook to replicate the functionality of componentDidMount()
, componentDidUpdate()
, and componentWillUnmount()
. We do this using useEffect()
. The first argument to this is a function we want to run after the component renders, and the second argument is an optional array. This array is called the dependency array, and causes the useEffect()
function input to only run when the component renders and one of the values in the dependency array changed between the current render, and the previous render. Returning a function from useEffect()
acts as componentWillUnmount()
, as this function will be run when the component unmounts.
Hooks can only be called in React function components, and never in normal JavaScript functions or React class components. They can also only be called in the top-level of the function component, never inside conditions, loops, or other functions.
In a talk at React Conf in 2018, the React team outlined some issues with React: in particular with class
components. In particular, they outlined three things which just kinda sucked in React up to 2018:
div
s wrapping a deeply nested component. class
components. For instance, in componentDidMount
we may subscribe to some network requests and set some recurring events, then in componentWillUnmount
we simply do the opposite; a negative mirror of sorts. Often these two functions repeat the same code, just to the opposite effect.componentDidMount(){
this.subscribeToNetwork(this.props.network);
this.startTimers();
}
...
componentWillUnmount(){
this.unsubscribeFromNetwork(this.props.network);
this.stopTimers();
}
class
es in JavaScript are just plain confusing. In particular, understanding why we need to bind this
in our constructor()
for methods we define on a class
. While this is a fascinating topic, it seems like a large mental barrier to overcome before we can get on with what we actually want to do: write web applications.Dan Abramov, an engineer on the React team, believed that these three things were all symptoms of one issue in React: That we must use a class
component whenever we want to introduce state
- regardless of how small or simple the component is. [2]
"React doesn't provide a stateful primitive simpler than a class component." - Dan Abramov, React Conf 2018
This is what Hooks aimed to fix: they allow state
to exist on a functional component.
The hooks system is how we can use state
and lifecycle methods in functional components. No constructor()
. No this
. Just plain old JS functions. Let's take state
for instance: how would we do this on a class
component?
import React from "react";
class App extends React.Component{
constructor(props){
super(props);
this.state = {data: ""};
}
render(){
return <div>Here's the data: {this.state.data}</div>
}
};
Well we'd need to first extend
from the React.Component
class to get access to those lifecycle methods, then we'd need to define our constructor()
and call super()
, passing in our props
so as not to lose access to those juicy lifecycle methods. Then, finally, we'd need to define a state
object using that pesky this
to store some piece of state we'd want to keep track of on the component.
Here's the equivalent using the hooks system:
import React, { useState } from "react";
const App = (props) => {
const [data, setData] = useState("");
return <div>Here's the data: {data}</div>
};
Yup, only a few lines of code. However, there is a lot to unpack in those few lines, so let's get stuck in!
What is a Hook? Well, useState()
above is a hook. As is useEffect()
. As is useCallback()
. Sensing a theme? Hooks are pretty easy to spot as the all have the naming convention of a use
prefix. These hooks allow us to hook into (hence the name) the state
and lifecycle of a component. These hooks are defined within the react
library for us.
The first thing we must do to access these hooks is to actually import these from the "react" library. We have two ways of doing this; we can either directly import
the hook we want (useState
in this case) from the "react" library using object destructuring syntax, or we can simply call useState
from the React
object we already imported. [3] For clarity, the following are exactly the same:
// Using the React.useState syntax
import React from "react";
React.useState("Initial value");
// Using object destructuring to call useState directly
import React, { useState } from "react";
useState("Initial value")
For this - and all - posts, I will tend to use the object destructuring syntax, however either is fine.
Similar to how we used object destructuring above to extract a specific value from an object, we can use array destructuring to a similar effect. This time, however, it is required. This uses the index of an array to assign values. [3] For instance:
const myArr = ["Football", "Rugby", "Badminton"];
Above, we defined an array with three values in it called myArr
: "Football"
at index 0, "Rugby"
at index 1, and "Badminton"
at index 2. When we use array destructuring, the value our variables take will index-match the values in myArr
. Therefore, say for instance we wanted to select "Football"
and "Rugby"
from the above array. We could do this by writing a variable in the same index as it appears in the array we assign from.
const myArr = ["Football", "Rugby", "Badminton"];
const [bestSport, worstSport] = myArr;
console.log(bestSport); // "Football"
console.log(worstSport); // "Rugby"
"Football"
is in array index 0 in mrArr
, therefore when we say const [bestSport] = myArr
, we are saying "Assign the first value in myArr to a variable called bestSport". When we say const [sport1, sport2] = myArr
, we are saying "Assign the value in the first index (0) of myArr to a variable called sport1, and the value in the second index of myArr (1) to a variable called sport2".
This is how the line const [data, setData]
is working. The useState()
function will return
an array, and we are just taking the first thing in the array returned from useState()
and assigning it to a variable we decided to call data
, and the second to a variable we decided to call setData
. The beauty of using array destructuring here is that we can pick the names for our variables, allowing us to choose names for the pieces of state we want (yes, these variables are actually state
!). Although, by convention, we normally use the naming scheme const [thing, setThing]
when defining these variables.
The real magic of the whole process happens with the call to useState()
. The under-the-hood workings of useState()
use a JS concept called closures which we will look at when we look at scoping another day, however luckily we do not need to know how it works in order to make use of it (we trade a complex JS topic like this
for an equally complex topic called closures, however in latter case we do not actually need to know anything about the topic to make use of it).
useState()
returns two things: the value we assign to our data
variable, and a function we assign to our setData
variable. The initial value for our data
variable is given as an argument to useState()
. [4] Below is a mental model for how to think about useState()
.
// primitive useState
function useState(initialValue){
let value = initialValue;
function setValue(newValue){
value = newValue;
}
return [value, setValue];
}
const [myState, setMyState] = useState("Josh");
// myState is assigned to be "value",
// and setMyState is assigned to be the function "setValue"
For clarity - below is state introduced on a class and a function component.
// Class component
class MyComponent extends React.Component{
contructor(){
super(props);
this.state = {name: "Josh"};
}
}
// Functional component using Hooks
const MyComponent = () => {
const [name, setName] = useState("Josh");
}
Okay, but what about that second value we get back from useState()
that we assign to setData
? This is a replacement for the this.setState()
method that we no longer have access to on a functional component. Where we would call this.setState()
to update any piece of state, with hooks we get a unique function for each piece of state we keep track of.
import React, { useState } from "react";
const App = (props) => {
const [count, setCount] = useState(0);
const [name, setName] = useState("Josh");
console.log(count); // 0 on the first render
setCount(5); // console.log above now outputs 5 as this causes a re-render
console.log(name); // "Josh"
setName("Emily"); // "Emily" logged out above after re-render
return(
<div></div>
);
};
We make a call to useState()
for each piece of state we want to keep track of, and in return we get a the piece of state as a variable (count
, name
), and a function specific to updating that one piece of state (setCount
, setName
).
Calling setThing()
( setCount
, setName
etc) also has the same effect as calling this.setState()
in a class
component: we trigger a re-render of the component. This also means we need to follow the same rule of never mutating state directly: we should never say count =
to update the value of count, only ever call setCount()
otherwise our component won't re-render. [5] It's worth noting that everything regarding component lifecycle is still relevant - it's just we're now using different functions to convey the stages. We still mount a functional hook component, we still update a functional hook component, and we still unmount at the end of the lifecycle.
For clarity, with a
class
component we definestate
asthis.state = {count: 0}
in theconstructor()
function. Then, to update our state, we callthis.setState({count: 1})
to change ourcount
to1
. With a function component using hooks, we set each piece of state separately using theconst [count, setCount] = useState(0)
syntax, and then update that piece of state using thesetCount(1)
syntax to set ourcount
to1
.
Much like there are many lifecycle methods in class
components, there are also many different hooks. Luckily, much like lifecycle methods, some are used far more often than others.
This is perhaps the most used hook in modern-day React apps. We have already explored this hook thoroughly above, and so we can say that this hook is used to create state
on our function components (therefore, React now has a stateful primitive simpler than a class
component!). [4]
useState
replacesthis.state
,this.setState()
, andconstructor()
.
I mentioned that we still mount, update, and unmount while using hooks, however by using functional component we lose access to those all important lifecycle methods: componentDidMount()
, componentDidUpdate()
and componentWillUnmount()
. This is where useEffect()
comes in. useEffect()
is all three of the aforementioned lifecycle methods rolled into one function. [6]
import React, { useState, useEffect } from "react";
const App = (props) => {
const [count, setCount] = useState(0);
useEffect( () => {
// arrow function as input for useEffect
});
return(
<div>
The count is {count}
</div>
);
};
The main part of the useEffect()
hook is the arrow function we pass as the first argument to it. This is a function we want to run every time the component renders itself (much like componentDidUpdate()
, except this will also run on the initial render too). We can also pass an optional second argument to useEffect()
- this is called the dependency array, which we will cover further down. This is why useEffect()
is kind of like a mix of componentDidMount()
(which runs on the first render) and componentDidUpdate()
(which runs on each subsequent render). Using a Counter
example:
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count = ${count}`;
});
return (
<div>
<p>The current count is {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Here, we setup count
to be state, and then create a <button />
which will increment the count upon clicking it (onClick={() => setCount(count+1)}
). On the initial render, the function in our useEffect()
hook runs and updates the title of our document to be the current count (0
).
After clicking the button to increment our count, our component will re-render and our useEffect()
will run again, updating our title to the new value of count
.
I also mentioned the dependency array, so let's touch on that too. The dependency array is a second (optional) argument to useEffect()
.
useEffect(someFunction, []);
This array allows us to control more precisely when the function we pass in to useEffect()
runs. Perhaps we don't want to run the useEffect()
every time the component re-renders; perhaps we have some specific useEffect()
that we only want to run when a specific piece of state changes. For instance, using the counter from above:
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const [onlineStatus, setOnlineStatus] = useState(false);
useEffect(() => {
document.title = `Count = ${count}`;
});
return (
<div>
<p>The current count is {count}</p>
<p>{onlineStatus === true ? "You're online!" : "You're NOT online!"}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setOnlineStatus(!onlineStatus)}>
Toggle online status
</button>
</div>
);
}
Here we have two buttons - one will update our count
, and one that will toggle our onlineStatus
(a nonsense piece of state for illustration purposes). When a user toggles their online status using the appropriate button, the component will re-render to display the new onlineStatus
value. This triggers our useEffect()
to run, which will update our document title to be the current value of count
. However, the count
hasn't changed since the last render, so there's no need to update the document.title
- running the useEffect()
function would be a waste of processing time.
To prevent this unnecessary calling on the useEffect()
we can specify the function in that particular useEffect()
should only be run when the count
changes.
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const [onlineStatus, setOnlineStatus] = useState(false);
useEffect(() => {
document.title = `Count = ${count}`;
}, [count]); // dependency array here
return (
<div>
<p>The current count is {count}</p>
<p>{onlineStatus === true ? "You're online!" : "You're NOT online!"}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setOnlineStatus(!onlineStatus)}>
Toggle online status
</button>
</div>
);
}
After every render, React will look at the dependency array of each useEffect()
. It will compare the previous values (before the re-render) of the variables inside the array to their current values (after the re-render). Then, if any of the values have changed, React will run that useEffect()
. If the most recent render didn't change the value of anything in the dependency array, React will choose not to run that useEffect()
. Therefore, we could have multiple pieces of state
in our dependency array e.g [a, b, c]
, and the function passed into that useEffect()
would only run if a
and/or b
and/or c
change. Pieces of state
isn't all we can put into our dependency array, we can also pass in props
too (remember, our component re-renders upon new state
and/or new props
). One final note on the dependency array, is that we can also pass as empty array to it.
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Only on the initial render")
}, []);
return (
<div>
<p>The current count is {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Passing an empty array tells React to only run this useEffect()
on the first render only, exactly the same way you'd use componentDidMount()
. Literally we are saying "only run this if any of the values in the dependency array change, however the dependency array is empty therefore no values in it can change, therefore it will only ever run on startup".
Note, we can have multiple calls to useEffect()
in one component. Perhaps - taking the counter from above - we want the document.title
to reflect whatever the recently changed piece of state was. We could add a second useEffect()
that would only run upon a change of onlineStatus
.
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const [onlineStatus, setOnlineStatus] = useState(false);
useEffect(() => {
document.title = "Count changed!";
}, [count]);
useEffect(() => {
document.title = "onlineStatus changed!";
}, [onlineStatus]);
return (
<div>
<p>The current count is {count}</p>
<p>{onlineStatus === true ? "You're online!" : "You're NOT online!"}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setOnlineStatus(!onlineStatus)}>
Toggle online status
</button>
</div>
);
}
Here, we use two useEffect()
calls with different dependency arrays. This allows us to update the document.title
to reflect the most recent state change.
So, now we have componentDidMount()
and componentDidUpdate()
sorted, how do we use useEffect()
to mimic componentWillUnmount()
? Well, notice how we haven't returned anything from our useEffect()
functions yet! They are all just functions that run when the component re-renders (according to their dependency array) and have no return statement. This is because the return
statement acts as componentWillUnmount()
. That is to say, when we return
a function from a useEffect()
, the function will run when the component unmounts. As an example, with a class-based component if we set an interval in componentDidMount()
we would have to clean it up again with componentWillUnmount()
.
import React from "react";
class App extends React.Component{
componentDidMount(){
this.interval = setInterval(() => alert("Hey I'm still here!"), 5000);
}
componentWillUnmount(){
clearInterval(this.interval);
}
render(){
return (
<div>
Hello, world!
</div>
);
}
};
However, we could achieve the same effect using the useEffect()
hook in a functional component!
import React, { useEffect } from "react";
const App = () => {
useEffect( () => {
let interval = setInterval(() => alert("Hey I'm still here!"), 5000);
return ( () => {
clearInterval(interval);
});
}, []);
return (
<div>
Hello, world!
</div>
);
};
Again, we set an interval in useEffect()
(with an empty dependency array so it only runs on the first render), and then we clean it up by return
ing a function from useEffect()
and calling clearInterval()
within it. This is the same as calling componentWillUnmount()
to clear the interval.
useEffect(function, [])
replacescomponentDidMount()
,componentDidUpdate()
, andcomponentWillUnmount()
.
useState()
and useEffect()
are by far the most common hooks. Much like lifecycle methods, others do exist and have their place, however these tend to be more niche and so will not be covered here. However, hooks can pretty much be used as a direct replacement for class components and their lifecycle methods.
Hooks are great, however they do come with two caveats [7]:
Top-level here refers to the main body of your functional component. This means Hooks cannot be called inside conditions, other functions, or loops. We cannot say "only use this hook if a condition is satisfied", or "call this hook within this loop".
import React, { useState, useEffect } from "react";
const App = () => {
if(/* some condition */){
const [thing, setThing] = useState("");
// NOT ALLOWED!
};
return (
<div>
Hello, world!
</div>
);
};
Attempting to call a hook outside of the main function body will cause an error. For clarity, I have shown explicitly where it top-level in a given functional component, and where is not.
import React, { useEffect } from "react";
const App = () => {
// Top level
// Top level
if(/* some condition */){
// NOT top level
};
// Top level
for (let i = 0; i < 3; i++){
// NOT top level
}
// Top level
const someFunc = () => {
// NOT top level
};
// Top level
return (
//NOT top level
<div>
Hello, world!
</div>
);
};
As a rule of thumb, if it's got curly braces/parenthesis around it that aren't the main curly braces/parenthesis for your function component, it's probably not top level.
For this reason, it's often good practice to declare all hooks right at the start of the component, before declaring any helper functions or other variables.
import React, { useState, useEffect } from "react";
const App = () => {
// all useState calls
// all useEffect calls
// Custom variables/functions
return (
<div>
Hello, world!
</div>
);
};
Hooks only exist inside a React function component - calling them inside a class
component or regular JS functions will not work.*
* We'll come back to this another day - calling hooks in regular JS functions aren't permitted, but we CAN call them from Custom Hooks, which are JS functions that use the Hook system.
The reasons have to do with how the Hook system works on a fundamental level. It relies on all hooks appearing in the same order every render.
const [one, setOne] = useState("one"); // Always first, every render
const [second, setSecond] = useState("second"); // Always second, every render
const [third, setThird] = useState("Third"); // Always third, every render
If we declare our hooks during the initial render in this order, React expects this order to stay the same on every render. If, instead, we were to conditionally call a hook:
const [one, setOne] = useState("one"); // First
if(/* condition */){
const [second, setSecond] = useState("second"); // Sometimes second.. sometimes not called
};
const [third, setThird] = useState("Third"); // Sometimes second, sometimes third
This could seriously mess up the order of our hooks. Note, this applies to useEffect()
as well; the below is also not permitted.
const [one, setOne] = useState("one"); // First hook
if(/* condition */){
useEffect(() => conosle.log("something")) // Sometimes second hook.. sometimes not called
};
const [third, setThird] = useState("Third"); // Sometimes second hook, sometimes third hook
Note: Even if the function we pass to
useEffect
is not invoked due to its dependency array,useEffect()
is still technically called and so wis fine for this purpose; dependency arrays will not cause an issue in regards to calling hooks in a different order.
Therefore, if we want to include conditions or loops, we put them inside our hook. For example:
if(name === "josh"){
useEffect(() => console.log("Hi josh"));
// NOT OKAY
};
useEffect(() => {
if (name === "josh") {
console.log("Hi josh");
// Okay!
};
});
// So long as the useEffect is called
// in the top level, we can put
// any conditions or loops inside the the useEffect
To close this section on hooks, let's look back on our Counter
component from a previous article.
index.js
import React from "react";
import ReactDOM from "react-dom";
class Counter extends React.Component {
constructor(props){
super(props);
this.state = {count : 0}
};
increment(){
this.setState({count: this.state.count + 1});
};
decrement(){
this.setState({count: this.state.count - 1});
};
render(){
return(
<div>
<h1>The count is: {this.state.count}</h1>
<button onClick={() => this.increment()}>Increment</button>
<button onClick={() => this.decrement()}>Decrement</button>
</div>
);
};
};
ReactDOM.render(
<Counter />,
document.querySelector("#root")
);
We can refactor (to re-write code into a different style to make it cleaner or obey different design principles) this to use hooks instead. [8]
Firstly, we convert our class
into a function, and import useState
.
index.js
import React, { useState } from "react";
// import useState
import ReactDOM from "react-dom";
const Counter = () => { // create a function instead of a class
constructor(props){
super(props);
this.state = {count : 0}
};
increment(){
this.setState({count: this.state.count + 1});
};
decrement(){
this.setState({count: this.state.count - 1});
};
render(){
return(
<div>
<h1>The count is: {this.state.count}</h1>
<button onClick={() => this.increment()}>Increment</button>
<button onClick={() => this.decrement()}>Decrement</button>
</div>
);
};
};
ReactDOM.render(
<Counter />,
document.querySelector("#root")
);
Next, instead of using a constructor()
to create our state, let's use the useState()
hook.
index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
const Counter = () => {
const [count, setCount] = useState(0); // useState instead of a constructor
increment(){
this.setState({count: this.state.count + 1});
};
decrement(){
this.setState({count: this.state.count - 1});
};
render(){
return(
<div>
<h1>The count is: {this.state.count}</h1>
<button onClick={() => this.increment()}>Increment</button>
<button onClick={() => this.decrement()}>Decrement</button>
</div>
);
};
};
ReactDOM.render(
<Counter />,
document.querySelector("#root")
);
Next, we can remove the increment
and decrement
methods as we can update our count
with setCount()
instead.
index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
const Counter = () => {
const [count, setCount] = useState(0);
// removed increment/decrement methods
render(){
return(
<div>
<h1>The count is: {this.state.count}</h1>
{/* setCount to increment/decrement */}
<button onClick={() => setCount(count + 1))}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
};
};
ReactDOM.render(
<Counter />,
document.querySelector("#root")
);
Then we can remove that render method that we don't need anymore.
index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
const Counter = () => {
const [count, setCount] = useState(0);
// render() removed
return(
<div>
<h1>The count is: {this.state.count}</h1>
<button onClick={() => this.increment()}>Increment</button>
<button onClick={() => this.decrement()}>Decrement</button>
</div>
);
};
ReactDOM.render(
<Counter />,
document.querySelector("#root")
);
Finally, we can clean up that this.state.count
to just be count
instead.
index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
const Counter = () => {
const [count, setCount] = useState(0);
return(
<div>
<h1>The count is: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
};
ReactDOM.render(
<Counter />,
document.querySelector("#root")
);
And there we have it! Again, this is the exact same as the Counter
component we made using class
es before, and you can copy-paste this into the index.js
of your CRA project folder and get a cool Counter app - this time with hooks instead! Now I'm not sure about you, but I think that looks way neater, and it's way easier to understand what's going on here compared to looking at a constructor()
, this
, and a state
object.
In summary, we have scratched the surface of the React Hook system. We saw how we can replace the lifecycle methods that we depended on class
components for, such as componentDidMount()
and componentWillUnmount()
, with the hooks system. We saw how useEffect()
can be used to replace three common lifecycle methods, and how we can use useState()
to introduce the idea of state
into our components, allowing us to re-render a component and keep track of a changing variable throughout renders. We also briefly covered object and array destructuring and how they are used with Hooks, and covered a brief comparison of class
components vs functional hook components. We did not cover custom hooks, as these are less important to know compared to useState()
and useEffect()
, and so can be left as a story for another day.