PS - This was originally posted in my blog. Check it out if you want learn more about React and JavaScript!
Until somewhat recently, if you wanted to use state in React you had to use a class component extending from either React.Component
or React.PureComponent
. The release of React 16.8 brought hooks which allowed state to be used in functional components.
If you wanted to do something like converting an existing class component to a functional component or fetch data in a functional component, you might be wondering how we can bring over the functionality of the life cycle methods. Three of the more popular methods, namely componentDidMount
, componentWillUnmount
and componentDidUpdate
, can all be implemented with a single hook, useEffect
.
componentDidMount
Say we have a component like this.
import React from "react"
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
posts: [],
}
}
componentDidMount = async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
).then(response => response.json())
this.setState({ posts: response })
}
render = () => {
return (
<div>
{this.state.posts.map(post => (
<div key={post.id}>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
))}
</div>
)
}
}
export default App
The example above is relatively straightforward. We fetch a list of posts once the component loads, the state with response and list them out.
If we write the same thing as a functional component.
import React from "react"
const App = () => {
const [posts, setPosts] = React.useState([])
React.useEffect(async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
).then(response => response.json())
setPosts(response)
}, [])
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
))}
</div>
)
}
export default App
Basically all we're doing is taking the code that was inside componentDidMount
and running it inside an effect.
The thing to remember with useEffect if that they run after each render. The second argument in useEffect
is used to control when to run the effect. The argument is an array of states which to run the effect after one of them is updated. To make sure the effect only runs once, an empty array is passed as the argument.
If you don't set the 2nd argument in a
useEffect
where the state is updated, an infinite loop will occur.
While the above effect will run as is, React will show a warning saying that "An effect function must not return anything besides a function, which is used for clean-up." since our effect returns a Promise. To fix this, we'll move the data fetching code to an asynchronous function outside the effect.
const fetchData = async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
).then(response => response.json())
setPosts(response)
}
React.useEffect(() => {
fetchData()
}, [])
The final code will look like this.
import React from "react"
const App = () => {
const [posts, setPosts] = React.useState([])
const fetchData = async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
).then(response => response.json())
setPosts(response)
}
React.useEffect(() => {
fetchData()
}, [])
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
))}
</div>
)
}
export default App
componentWillUnmount
To show how we can implement componentWillUnmount
with hooks, let's consider the following example where we create a event listener to check for the window dimensions after the component has mounted and removing the listener when the component is about to unmount.
import React from "react"
export default class App extends React.Component {
state = { width: 0, height: 0 }
updateDimensions = () => {
this.setState({ width: window.innerWidth, height: window.innerHeight })
}
componentDidMount = () => {
window.addEventListener("resize", this.updateDimensions)
}
componentWillUnmount = () => {
window.removeEventListener("resize", this.updateDimensions)
}
render = () => {
return (
<span>
Window size: {this.state.width} x {this.state.height}
</span>
)
}
}
First let's create the functional component with just the state and jsx.
const App = () => {
const [width, setWidth] = React.useState(0)
const [height, setHeight] = React.useState(0)
return (
<span>
Window size: {width} x {height}
</span>
)
}
export default App
Next we'll create the function used to update the states.
const updateDimensions = () => {
setWidth(window.innerWidth)
setHeight(window.innerHeight)
}
After that we'll create an event listener using useEffect
like we did using componentDidMount
in the class component.
React.useEffect(() => {
window.addEventListener("resize", updateDimensions)
}, [])
Notice how we set the second argument as an empty array for useEffect
to make sure is runs only once.
Once we set up the event listener its important that we remember to remove the listener when needed to prevent any memory leaks. In the class component this was done in componentWillUnmount
. We can achieve the same thing in hooks with the cleanup functionality in useEffect
. useEffect
can return a function which will be run when it is time to cleanup when the component unmounts. So we can remove the listener here.
React.useEffect(() => {
window.addEventListener("resize", updateDimensions)
return () => {
window.removeEventListener("resize", updateDimensions)
}
}, [])
The return of useEffect
being reserved for the cleanup function is the reason why we got an error in the componentDidMount
example initially when when we made the function inside useEffect
async
as it was returning a Promise.
The final code will be like this.
import React from "react"
const App = () => {
const [width, setWidth] = React.useState(0)
const [height, setHeight] = React.useState(0)
const updateDimensions = () => {
setWidth(window.innerWidth)
setHeight(window.innerHeight)
}
React.useEffect(() => {
window.addEventListener("resize", updateDimensions)
return () => {
window.removeEventListener("resize", updateDimensions)
}
}, [])
return (
<span>
Window size: {width} x {height}
</span>
)
}
export default App
componentDidUpdate
Finally for componentDidUpdate
let's look at this component.
import React from "react"
export default class App extends React.Component {
state = {
id: 1,
post: {},
}
getPost = async id => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`
).then(response => response.json())
this.setState({ post: response })
}
setId = id => this.setState({ id })
componentDidMount = () => {
this.getPost(this.state.id)
}
componentDidUpdate = (prevProps, prevState) => {
if (this.state.id !== prevState.id) {
this.getPost(this.state.id)
}
}
render = () => {
return (
<div>
<span>
<button
disabled={this.state.id === 1}
onClick={() => this.setId(this.state.id - 1)}
>
-
</button>
{this.state.id}
<button
disabled={this.state.id === 100}
onClick={() => this.setId(this.state.id + 1)}
>
+
</button>
</span>
<h1>{`${this.state.post.id} - ${this.state.post.title}`}</h1>
<p>{this.state.post.body}</p>
</div>
)
}
}
In the above example we fetch a post once when the component mounts in componentDidMount
and then every time the id gets updated in componentDidUpdate
.
To get started on turning this into a functional component, let's first write the following code declaring the states and the returning jsx.
import React from "react"
const App = () => {
const [id, setId] = React.useState(1)
const [post, setPost] = React.useState({})
return (
<div>
<span>
<button disabled={id === 1} onClick={() => setId(id - 1)}>
-
</button>
{id}
<button disabled={id === 100} onClick={() => setId(id + 1)}>
+
</button>
</span>
<h1>{`${post.id} - ${post.title}`}</h1>
<p>{post.body}</p>
</div>
)
}
Then let's declare a function to retrieve a post.
const getPost = async id => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`
).then(response => response.json())
setPost(response)
}
Next we need to think about retrieving the first post when the component mounts. We can do this in a useEffect
with the second argument being an empty array.
React.useEffect(() => {
getPost(id)
}, [])
The component should load a new post when the ID is changed. The second argument in useEffect
is a list of states for which the effect should run when once of them updates. So to run the effect again when the ID changes, we can add the ID to the array.
React.useEffect(() => {
getPost(id)
}, [id])
Finally your component should look like this.
import React from "react"
const App = () => {
const [id, setId] = React.useState(1)
const [post, setPost] = React.useState({})
const getPost = async id => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`
).then(response => response.json())
setPost(response)
}
React.useEffect(() => {
getPost(id)
}, [id])
return (
<div>
<span>
<button disabled={id === 1} onClick={() => setId(id - 1)}>
-
</button>
{id}
<button disabled={id === 100} onClick={() => setId(id + 1)}>
+
</button>
</span>
<h1>{`${post.id} - ${post.title}`}</h1>
<p>{post.body}</p>
</div>
)
}
export default App
As you can see we can take of the both the componentDidMount
and componentDidUpdate
functionality in one useEffect
which cuts down on duplicating code.`
Wrapping up
I hope you find this post useful on how to achieve some of the functionality in class components in functional components. If you want to learn more about hooks, the React documentation has a great introduction to hooks here.