There are two ways to create React components, using functions or classes. A common question when learning React is often “When do I use one versus the other?” Luckily, this is a question that might eventually become obsolete with the release of React version 16.8 as it includes hooks.
Hooks provide a way to use functionality such as state and context that could only be achieved through classes previously to be easily done with functional components.
In this blog, we’ll introduce React hooks and show some code examples of those hooks in action. Specifically, we will take a simple class component and convert it to a function with hooks, have an in-depth look at hooks useState
and useEffect
, and create a custom hook. Let’s get started!
State with useState
Local state is probably the most common reason to use a class component. So, let’s see how to take a simple class component and convert it to a function with hooks.
import React, { Component } from 'react'; class InputClass extends Component { state = { text: '' } onInputChange = (e) => { this.setState({text: e.target.value}) } render() { return ( <input value={this.state.text} onChange={this.onInputChange} /> ) } } export default InputClass;
Simple enough, but how can we use a functional component?
import React, { useState } from 'react'; export default () => { const [text, setText] = useState(''); return ( <input value={text} onChange={(e) => {setText(e.target.value)}} /> ); }
We imported useState
from React. Invoked it within the functional component passing the initial state for our input. The hook returns us two items though. The first being the current value of our state and a function for setting it. This function will accept either the new state, or a function for setting it that will be passed the previous value.
Easy enough, right? If the state for your component requires more than a single item for state, you could pass an object to useState
.
import React, { useState } from 'react'; export default () => { const [state, setState] = useState({ text: '', count: 0 }) return ( <div> <input value={state.text} onChange={(e) => { const value = e.target.value; setState(prevState => { return { ...prevState, text: value } }); }} /> <div>{state.count}</div> <button onClick={() => { setState(prevState => { return { ...prevState, count: prevState.count + 1 } }) }} > Increment </button> </div> ); }
This works fine, but note that we had to merge the previous state in our returns for setState
. Those already familiar with using setState
on class components might expect it to merge the previous state automatically, but this is not the case.
In this instance, count and text do not really have anything to do with each other. So, it probably makes more sense to invoke useState
twice, once for count, and once for text. We can name the variables and functions returned from useState
anything we want.
import React, { useState } from 'react'; export default () => { const [text, setText] = useState(''); const [count, setCount] = useState(0); return ( <div> <input value={text} onChange={(e) => { setText(e.target.value) }} /> <div>{count}</div> <button onClick={() => { setCount(prevCount => prevCount + 1) }} > Increment </button> </div> ); }
Lifecycle Methods with useEffect
I would say the second most common reason to require using a Class
component is the use of lifecycle methods like componentDidMount
, componentWillUnmount
, componentDidUpdate
. One of the nice things about hooks is you don’t need to remember any of those names. Similar functionality can all be achieved with the useEffect
hook.
Let’s start with a Class
component that has some common uses of lifecycle components. Say we have an API that takes user input for a search and we want to display the results. This component will get passed the query to search from another component. So we would need to call the API when it mounts, and also whenever the query updates. Assume fetchResults
is just a method that calls the API and returns a promise that will resolve with the results.
import React, { Component } from 'react'; import { fetchResults } from './api'; class SearchResults extends Component { state = { results: [] } componentDidMount() { fetchResults(this.props.query) .then(results => { this.setState({results}) }); } componentDidUpdate(prevProps) { if (this.props.query !== prevProps.query) { fetchResults(this.props.query) .then(results => { this.setState({results}) }); } } render() { return ( <div> { this.state.results.map(result => { return ( // Display the results from the search ) }) } </div> ) } } export default SearchResults;
Not so bad, but we end up having fairly similar code in both lifecycle methods. Granted, we could break this down into a separate method, but we would still need to call it in both places. Let’s try it using the new useEffect
hook instead.
import React, { useState, useEffect } from 'react'; import { fetchResults } from './api'; export default ({query}) => { const [results, setResults] = useState([]); useEffect(() => { fetchResults(query) .then(setResults) }, [query]); return ( <div> { results.map(result => { return ( // Display the results from the search ) }) } </div> ) }
So here we achieve the same result with only having to refer to the code that fetches the query once. The function we pass will get run on initial render and whenever the query from props changes.
How does it know to only run the search when the query changes, I hear you ask? By the second parameter, we passed. useEffect
takes a second argument that should be an array of the dependencies it has. Since we only want to call the API when query changes, we put it in the array. Leaving the dependencies array out would cause the effect to run every render.
So this covers componentDidMount
and componentDidUpdate
. Now what if we wanted to clean something up after the component unmounted like a setTimeout
.
Let’s expand the previous example by making it a bit smarter. Maybe we don’t want the API request being made every time the user updates the query text; we should probably wait a bit to make sure they are done typing. If they happen to be navigating away before the request is made, for example by clicking on a result, we wouldn’t want to send out another request or run into a timing where we called setResults
after the component unmounted.
import React, { useState, useEffect } from 'react'; import { fetchResults } from './api'; export default ({query}) => { const [results, setResults] = useState([]); useEffect(() => { let isCleanedUp = false; const timeoutId = setTimeout(() => { fetchResults(query) .then(results => { !isCleanedUp && setResults(results); }) }, 1000); return () => { clearTimeout(timeoutId); isCleanedUp = true; }; }, [query]); return ( <div> { results.map(result => { return ( // Display the results from the search ) }) } </div> ) }
The effect still runs every time the query changes, our dependencies haven’t changed. But now we return a function that does a clearTimeout
for the timer we made to send the API request one second after the query prop changes. We also check isCleanedUp
before calling setResults
to avoid any timing issues. If you return a function from useEffect
, React will invoke it before the effect is run again, as well as when the component unmounts. Use it to clean up anything necessary from the previous effect.
useEffect
can take a bit to get used to as you truly need to consider all of the dependencies your effect needs. Anything that could change between renders should be stated in its dependencies. This includes variables from useState
or even functions. Note that update function from useState
setResults
gets a pass on this since React ensures this function is static.
useState
and useEffect
should cover most scenarios you would need to use a Class component as opposed to a functional one, but there are other React hooks.
useContext(MyContext)
useContext
Will subscribe a component to the nearest provider of MyContext
and cause it to render whenever context changes.
useReducer
is worth looking at for components with complex state. I won’t go into the details in this blog, but if you’re familiar with redux style reducers, this should give you an idea.
const [state, dispatch] = useReducer(reducer, initialState);
So now we’re ready to use hooks and forget about Class components, right? But what problem are we actually solving by doing this? You might say it’s just another way of doing the same stuff that we were already doing with classes.
There are a few benefits I think we can already see at this point, though. Typically, you should be able to get by with a little less code using hooks as opposed to classes. Only using function means you’ll never have to convert a component from function to class again or vice versa. If you need state in a component that didn’t have it before, you can simply add the useState
hook.
JavaScript classes, in general, can be a challenge for those new to JS. this
doesn’t behave like this
in most other languages. I used arrow functions for the examples in this blog, but even if you do not, you would not have to worry about binding callbacks without classes.
Custom Hooks
However, I think the real benefit comes in when you start writing custom hooks. Let’s look at an example.
Let’s take all the code that we used in our search component and move it to a custom hook.
import React, { useState, useEffect } from 'react'; import { fetchResults } from './api'; export default (query) => { const [results, setResults] = useState([]); useEffect(() => { let isCleanedUp = false; const timeoutId = setTimeout(() => { fetchResults(query) .then(results => { !isCleanedUp && setResults(results); }) }, 1000); return () => { clearTimeout(timeoutId); isCleanedUp = true; }; }, [query]); return [results]; }
Nothing complicated. We mostly just copied over the state and effect logic from the component. The difference is we are returning an array containing the search results. If needed, we could have returned multiple things. Now we import and invoke it in our searchResults
component.
import React from 'react'; import useSearch from './useSearch'; export default ({query}) => { const [results ] = useSearch(query); return ( <div> { results.map(result => { return ( // Display the results from the search ) }) } </div> ) }
There’s a couple of rules you should follow when using custom hooks. The naming convention use...
should be followed. This allows React to know you’re using a hook. And hooks can only be called from the top level of your function. They should never be inside another scope. React relies on the order that the hooks that are invoked to remain consistent in order to keep track of them, therefore you cannot conditionally run a hook. Instead, try to find a why to move the condition inside of your hook.
Custom hooks give us a nice way to abstract stateful logic, leaving our component less cluttered. We could reuse the useSearch
hook anywhere else in our application that it may be needed. There were ways to accomplish this without hooks (with patterns like HOCs or render props), but compared to hooks they are complicated and hard to follow.
Wrap Up
So now we have seen how to use React hooks to manage state and effects in our components without the use of classes. We have also covered how to abstract our stateful logic with reusable custom hooks that can help us write clean, easy to understand components.
I hope that you’ve found this information helpful!