Hedegare's Blog

Building - Learning - Optimizing - Growing

Beginner

Javascript

React

React Hooks

Web Development

Introduction to React Hooks

React has revolutionized the way user interfaces are built, and with the introduction of hooks, it has become even more powerful and intuitive. Whether you are new to React or have some experience, understanding hooks is essential for modern React development. In this post, we will break down the basics of React hooks, explaining what they are, and how you can start using them to manage state and side effects in your web applications. This guide will also provide explanations and examples of the most common built-in React hooks. By the end of this guide, you will have a solid foundation to start incorporating hooks into your React projects with confidence.


What are React Hooks?

React hooks provide functional components with the ability to use state and manage side effects, allowing developers to add state and other React features without having to be limited to a class component. They provide a cleaner, easier-to-understand, and more concise way to handle state and side effects in React applications.

Hooks can only be called at the top level of your components, this means that you can’t call hooks inside conditions, loops, or other nested functions. Think about it this way, you “call” hooks at the top of your component, similar to how you “import” modules at the top of your file.

We will now explore some hooks already available in React.

State Hooks

When a component needs to store information, for example, an input form, we can use state to hold that data. To add state to a component, use one of this hooks:

useState example

Using a simple example to demonstrate how to implement the useState hook, here we’ll have a component with a button that holds the amount of times it has been pressed; that information is stored in the count variable and is updated through the setCount function.

useState.js
1
import { useState } from "react";
2
3
export default function App() {
4
const [count, setCount] = useState(0);
5
6
return (
7
<button onClick={() => setCount(count + 1)}>
8
You clicked { count } times.
9
</button>
10
);
11
}

useReducer example

On the surface, useReducer is very similar to useState, but it lets you move the state update logic from multiple event handlers into a single function.

In this example, we’ll have a component with a form, and we’ll make use of the useReducer hook to update it; the form is made of a text input where the user can type a name and two buttons for the user to increment or decrement an age value.

useReducer.js
1
import { useReducer } from "react";
2
3
function reducer(state, action) {
4
switch(action.type) {
5
case "changed_name": {
6
return {
7
...state,
8
name: action.newName
9
};
10
}
11
case "incremented_age": {
12
return {
13
...state,
14
age: state.age + 1
15
};
16
}
17
case "decremented_age": {
18
return {
19
...state,
20
age: state.age - 1
21
};
22
}
23
}
24
}
25
26
export default function App() {
27
const [state, dispatch] = useReducer(reducer, { name: "John", age: 25 });
28
29
return (
30
<>
31
<input value={state.name} onChange={(e) => dispatch({ type: "changed_name", newName: e.target.value })} />
32
<div>
33
<button onClick={() => dispatch({ type: "decremented_age" })}>Decrement age</button>
34
<button onClick={() => dispatch({ type: "incremented_age" })}>Increment age</button>
35
</div>
36
<p>Hello { state.name }, your age is { state.age }.</p>
37
</>
38
);
39
}

Depending on what element the user interacted with, an action will be dispatched with a type, and that type is what will determine what will happen to the state of our component.

Context Hooks

In React, passing data from a parent component to its children can be troublesome, especially if those children are too deep into the component tree. This is called prop drilling, which is the process of passing data through multiple levels of components.

To pass information to a component from a distant parent without prop drilling, you can use context. A top-level component can pass its current context to all components below, no matter how deep they are, using a context hook.

useContext example

A common use case for the useContext hook is applying a dynamic theme to your app. In this example, we’ll demonstrate how to implement a light/dark mode toggle using this hook.

As you’ll see below, the parent component, called App is the one that holds the information on what theme is active; that theme is stored using a useState hook, that is passed through context to all children. All components that need to know the value of theme need to be wrapped in a Context.Provider component that we create using the createContext function.

useContext.js
1
import { createContext, useContext, useState } from "react";
2
3
const ThemeContext = createContext();
4
5
export default function App() {
6
const [theme, setTheme] = useState("light");
7
8
return (
9
<ThemeContext.Provider value={theme}>
10
<Header />
11
<main>
12
<Sidebar />
13
<Body />
14
</main>
15
<Footer />
16
<label><input type="checkbox" onChange={(e) => { setTheme(e.target.checked ? 'dark' : 'light') }} />Use dark mode</label>
17
</ThemeContext.Provider>
18
);
19
}
20
21
const Header = () => {
22
const theme = useContext(ThemeContext);
23
return <header className={theme}>Header</header>;
24
}
25
26
const Sidebar = () => {
27
const [theme] = useState("dark");
28
return <div className={theme}>Sidebar</div>;
29
}
30
31
const Body = () => {
32
const theme = useContext(ThemeContext);
33
return <div className={theme}>Body</div>;
34
}
35
36
const Footer = () => {
37
const theme = useContext(ThemeContext);
38
return <footer className={theme}>Footer</footer>;
39
}

In this example, we have a simple app structure with an <Header />, an <Sidebar />, a <Body /> and a <Footer />, followed by a checkbox. This checkbox is what allows the user to change the app theme. When the user checks or unchecks the checkbox, all components that subscribe to the useContext hook will have their theme changed. If you try this code, you will see that the <Sidebar /> component does not change and is always using the “dark” theme. This is because that component has its own theme defined by the useState hook.

To finalize, let’s type some CSS to show you what the classes .light and .dark are in terms of code.

styles.css
1
header, div, footer {
2
padding: 10px;
3
margin: 10px;
4
}
5
6
main {
7
display: flex;
8
}
9
10
.light {
11
background-color: #ffffff;
12
color: #141414;
13
}
14
15
.dark {
16
background-color: #141414;
17
color: #ffffff;
18
}

This CSS snippet does not add any information for you to understand how to use and apply the useContext hook, I just like to show all the code I used to offer more context and completeness to my example.

Ref Hooks

When you want a component to hold some information, like a DOM node, but you don’t want to trigger new renders, you should use a ref. Unlike state, updating a ref does not trigger a re-render of your component.

Refs are very useful when you need to work with non-React systems, like built-in browser APIs.

useRef example

Now, let’s see a simple example, where we will use the useRef hook to focus an <input /> element when we press a button.

useRef.js
1
import { useRef } from "react";
2
3
export default function App() {
4
const inputRef = useRef(null);
5
6
const focusInput = () => {
7
inputRef.current.focus();
8
}
9
10
return (
11
<>
12
<input ref={inputRef} />
13
<button onClick={focusInput}>Focus input</button>
14
</>
15
);
16
}

Like I mentioned, this is a really simple example, but enough to demonstrate the power of the useRef hook. In this example, we hold an <input /> element, with the hook created with a starting value of null, using ref={inputRef}; then, with a function that is called on each button click, using plain JavaScript, we focus that DOM element.

This hook is very useful for cases when you don’t want to trigger an app re-render. In this case, this is exactly what happens. When the function focusInput is called, React does not trigger a re-render.

Effect Hooks

Effect hooks are useful when a component needs to deal with network, browser DOM, animations, even interact with a non-React component; they allow you to run code after rendering.

As an example, let’s say your component needs to fetch data from a remote server. To accomplish this task, an effect hook would be the best tool to use. A data fetch is an asynchronous task, so an effect hook makes sure that the remote call only runs after the component has rendered. This improves the user experience significantly.

useEffect example

For this example, I will show how to make calls to an API to fetch some Chuck Norris jokes, using the useEffect hook.

useEffect
1
import { useEffect, useState } from "react";
2
3
export default function App() {
4
const [joke, setJoke] = useState(null);
5
6
useEffect(() => {
7
fetch("https://api.chucknorris.io/jokes/random")
8
.then(response => response.json())
9
.then(data => setJoke(data.value));
10
}, []);
11
12
return (
13
<p>{joke}</p>
14
);
15
}

If you try this code, you will see that a new joke is shown every time the app is reloaded. This happens because of a simple piece of code, []. This is the second argument of the useEffect function, and represents an array of dependencies of the hook; when a dependency changes its value, the setup function, the first argument of the useEffect function, is executed.

When, like in this case, the dependencies array is empty, the setup function is executed only one time, during the first render of the app.

If you want to run the effect every time the app re-renders, you just omit the dependencies’ argument.

Performance Hooks

Optimization should be a concern if, for example, your app does lots and very complicated calculations, which can make the app very slow and unresponsive for some time. A common way to optimize re-rendering performance is to skip unnecessary work. For example, you can reuse a cached calculation or skip a re-render if data has not changed using performance hooks, like:

useMemo example

Here, we will have a function that sums all values in an array. As we add more numbers to the array, the more time it will take for the code to execute the function that sums all numbers (even though this time can be negligible in some cases, like in this example), and that will have performance implications in your app. So, to prevent that from happening, we can cache the result of that function and just execute it when necessary.

useMemo.js
1
import { useMemo, useState } from "react";
2
3
export default function App() {
4
const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
5
const [inputValue, setInputValue] = useState("");
6
7
const addNumber = () => {
8
setNumbers([...numbers, Math.floor(Math.random() * 100)]);
9
}
10
11
const sum = useMemo(() => {
12
console.log("Calculating sum...");
13
return numbers.reduce((a, b) => a + b, 0);;
14
}, [numbers]);
15
16
return (
17
<>
18
<h1>Sum:{sum}</h1>
19
<button onClick={addNumber}>Add number</button>
20
<input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
21
</>
22
);
23
}

If you open your browser’s console, you should see the message “Calculating sum…” (you should see the sum number too) every time the user clicks the button to add a number, meaning that the calculation was executed. But, if you try to change the input value, that message is not shown, that’s because the calculation is only executed when one dependency changes its value; in this case that dependency is represented by [numbers].

For learning purposes, I recommend you to try to remove the dependencies array from the useMemo function and see what happens. Spoiler alert, the result you will see is the reason this hook is so powerful and useful for making ours apps perform better.

useCallback example

This hook is similar to useMemo, but instead of caching a value, it caches a function. In this example, we will see a list of items where we can filter it using a text input.

useCallback.js
1
import { useCallback, useState } from "react";
2
3
export default function App() {
4
const [items] = useState(["Apple", "Banana", "Orange", "Grapes", "Mango"]);
5
const [filter, setFilter] = useState("");
6
7
const filteredItems = useCallback(() => {
8
return items.filter((item) => item.toLowerCase().includes(filter.toLowerCase()));
9
}, [filter]);
10
11
return (
12
<>
13
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
14
<ul>
15
{
16
filteredItems().map((item, index) => (
17
<li key={index}>{item}</li>
18
))
19
}
20
</ul>
21
</>
22
);
23
}

When the filter value changes, the function cached is recalculated (or recreated) with the new filter changes; if the user does not change the value of the <input /> element, the function is already cached and is ready to be executed, making the code (even though unnoticeable in this example since it is very simple) faster by avoiding unnecessary recomputation.

Although this example is very simple, it shows how the useCallback hook works and its advantages to optimize our components.

Custom Hooks

React comes with several built-in hooks, not just the ones I have listed above, but sometimes you might need a hook for something more specific to your needs. For that purpose, React allows you to create your own custom hooks.

Custom hook example

Let’s create a custom hook called useFetch, which will fetch data from an API and handle loading and error states. Then, we will use this hook in two components; one to display a list of users, and another to show a list of posts.

customHooks.js
1
import { useEffect, useState } from "react";
2
3
const useFecth = (url) => {
4
const [data, setData] = useState(null);
5
const [loading, setLoading] = useState(true);
6
const [error, setError] = useState(null);
7
8
useEffect(() => {
9
if (!url) return;
10
11
setLoading(true);
12
setError(null);
13
14
fetch(url)
15
.then((response) => {
16
if (!response.ok) {
17
throw Error("Could not fetch data.");
18
}
19
return response.json();
20
})
21
.then((data) => {
22
setData(data);
23
setLoading(false);
24
})
25
.catch((error) => {
26
setError(error.message);
27
setLoading(false);
28
});
29
}, [url]);
30
31
return { data, loading, error };
32
}
33
34
export default function App() {
35
return (
36
<>
37
<UserList />
38
<PostList />
39
</>
40
);
41
}
42
43
const UserList = () => {
44
const { data, loading, error } = useFecth("https://jsonplaceholder.typicode.com/users");
45
46
if (loading) return <p>Loading...</p>;
47
48
if (error) return <p>Error: {error.message}</p>;
49
50
return (
51
<>
52
<h1>User List</h1>
53
<ul>
54
{data.map((user) => (
55
<li key={user.id}>{user.name}</li>
56
))}
57
</ul>
58
</>
59
);
60
}
61
62
const PostList = () => {
63
const { data, loading, error } = useFecth("https://jsonplaceholder.typicode.com/posts");
64
65
if (loading) return <p>Loading...</p>;
66
67
if (error) return <p>Error: {error.message}</p>;
68
69
return (
70
<>
71
<h1>Post List</h1>
72
<ul>
73
{data.map((user) => (
74
<li key={user.id}>{user.title}</li>
75
))}
76
</ul>
77
</>
78
);
79
}

The custom hook useFetch handles data fetching, manages loading and error states, and returns these states along with the fetched data. This allows components using the hook to easily access the data’s loading status, any errors that occurred during the fetch, and the data itself. By abstracting this logic into a hook, components like <UserList /> and <PostList /> can focus on rendering UI elements based on the data without having to manage the fetch logic themselves.

Conclusion

React hooks offer a powerful way to interact with your components, they simplify the code, making it more readable and maintainable. By using hooks, developers can avoid the complexities associated with class components and lifecycle methods, leading to a cleaner and more efficient React applications.