Hook, line, and sinker
Or How I Learned to Stop worrying and love React hooks
Table of contents
Following up on the Beginner's guide to React, this article will cover the basic "hooks" that React offers.
What are hooks?
Hooks are a functionality introduced in React 16.8. They allow you to create stateful functional components.
If that last phrase made you have the same reaction as Al here, then this article's for you!
Let me try to explain it this way. Before, if we wanted to have state in a component, we'd need to use a class component
that would look something like this:
export default class TodoList extends React.Component {
state = {
todos: []
}
render() {
return (
<ul>
{this.state.todos.map(e => <li>{e}</li>)}
</ul>
)
}
}
But now there's no need to write something so long! With React hooks functional components can have state! Whereas before you would just use a functional component as a stateless one (where data comes from props, basically), you can now give state to those components. Let's take the previous example; using hooks, we can rewrite it like this:
const TodoList = () => {
const [todos] = useState([])
return (
<ul>
{todos.map(e => <li>{e}</li>)}
</ul>
)
}
//Export the function here
NB: I prefer to write functional components this way for readability, but you could also export the function directly.
Is it just me or did that code become a bit more readable just now? If you're wondering about the [todos]
part, I'll talk more about it in the next section. Speaking (or writing, such as it is) about that, let's dive into React hooks!
UseState
the useState
hook replaces the state
in a class component. It is used to define part of a component's state. Why do I say part of? Because you can (and should) use useState
multiple times in a single component. This allows you to have more control over the state and, when used in conjunction with the next hook (spoiler), will allow you to rerun code or rerender of your app on a specific state change.
Let's take the last bit of code and continue from there:
const TodoList = () => {
const [todos] = useState([])
return (
<ul>
{todos.map(e => <li>{e}</li>)}
</ul>
)
}
At this moment, the component renders a list of todos, but you might notice a problem; we have no way to set those todos.
To do that, we need to understand the return values of useState
: this function returns an array containing the state's value and a function to update said values. We can call it (and the value, for that matter) whatever we want, but let's stick to the conventions for now.
Let's update the code we've written before:
const TodoList = () => {
const [todos, setTodos] = useState([])
return (
<ul>
{todos.map(e => <li>{e}</li>)}
</ul>
)
}
Now we have a function to set our todos. Let's add an input and a button to add todos. In the input, we'll listen to the onChange
event to modify some other piece of state.
const TodoList = () => {
const [todos, setTodos] = useState([])
const [newTodo, setNewTodo] = useState("")
return (
<div>
<ul>
{todos.map(e => <li>{e}</li>)}
</ul>
<input onChange={(e) => {setNewTodo(e.target.value)}} value={newTodo} />
<button onClick={() => {
setTodos((oldTodos) => [...oldTodos, newTodo])
setNewTodo("")
}}>
Add Todo
</button>
</div>
)
}
Yeah, that's a lot of new stuff to cover, I know. Let's break it down
const [newTodo, setNewTodo] = useState("")
This is just a new useState. Notice how this time I put "" as a first argument. The first argument in a useState function call defines the starting value of the state and is also used to determine its type. This can be used by us (the developers) to get IntelliSense on the state (autocompletion etc).
<input onChange={(e) => {setNewTodo(e.target.value)}} value={newTodo} />
This input does two things: it reads the state newTodo and uses it as value while also updating it with its new value (when a user types in the input, the state will change to reflect that change).
<button onClick={() => {
setTodos((oldTodos) => [...oldTodos, newTodo])
setNewTodo("")
}}>
Add Todo
</button>
This button add the newTodo to the list and resets the input's text (i.e. newTodo). You might have noticed something...
setTodos((oldTodos) => [...oldTodos, newTodo])
setNewTodo("")
When you set a state, you can either directly pass the new value as an argument, or use an arrow function to get a reference to the current state's value (useful for updating arrays).
And that's it for the useState hook !!
useEffect
Now that we know how to set state in a component, let's see how to replace a componentDidUpdate
using hooks. the componentDidUpdate
method of a class component was used to trigger effects based on state and props change.
Let's say we're building a photo search app and we need to update the search results based on the current value of an input field. As the user types in the input field, we want to update the result. We could do it in the onChange
callback, but that would make the rerender dependent on the onChange and the app would become laggy (at least it used to, don't quote me on that).
Let's setup our basic component:
const PhotoSearch = () => {
const [searchValue, setSearchValue] = useState("")
const [photoResult, setPhotoResult] = useState([])
return (
<div>
<input
value={searchValue}
onChange={(e) => {setSearchValue(e.target.value)}}
/>
<div>
{photoResult.map((e) => (
<img src={e} />
))}
</div>
</div>
)
}
So, nothing new for now; we have to states, searchValue and photoResult, the former to store the input's value and the second to store the photos matching that search. Now let's use the useEffect
hook to update the photoResult array with our search results (here I'm using a basic example, a real API would have more complex data formats (like unsplash)
const PhotoSearch = () => {
const [searchValue, setSearchValue] = useState("")
const [photoResult, setPhotoResult] = useState([])
useEffect(() => {
if (searchValue) {
fetchImages(searchValue).then((data) => {
setPhotoResult(data)
})
}
}, [searchValue])
return (
<div>
<input
value={searchValue}
onChange={(e) => {setSearchValue(e.target.value)}}
/>
<div>
{photoResult.map((e) => (
<img src={e} />
))}
</div>
</div>
)
}
NB: fetchImages acts as a function that returns an array of links to images from an API
Let's break it down:
useEffect
takes two arguments (the second being optional): the former is a callback function and the latter is a dependency array.
The callback function gets executed every time the values inside the dependency array change. In our example, we want to rerun that function each time the searchValue
changes, so we add searchValue
to the dependency array. Notice also the condition inside the callback:
if (searchValue)
Since the value could be empty after a user deletes its input or when the page first loads, we need to check before executing a code that depends on it.
There are other quirks with useEffect, and you can read more about them on the official React documentation.
Final words
These are just the two hooks you'll use the most. Others exist (useContext, useCallback, useMemo), and you can even make your own. We'll probably look at those another time, on another article, but let's stop here for the time being.