DEV Community

Cover image for Custom Hooks | React.js
Vinícius Bueno Costa
Vinícius Bueno Costa

Posted on • Edited on

Custom Hooks | React.js

Custom Hooks what do they eat, how are they born, let's find out?

Summary

In this article, I will discuss a bit about the usefulness of Custom Hooks in React.js, how and when to create them. Additionally, some examples of their applications will be provided.

Introduction

Custom Hooks are helper functions that can only be used within React components, they work as a way to share logic among our components, allowing us to 'trim down' some of our code, making it cleaner and easier to maintain.

How do they work exactly?

When we think about making code cleaner, most of the time, we are looking for a way to reuse logic that has already been written, such as state initialization for our form and its various functions.

Presenting the problem:

Let's suppose we want an application where we have some items in a list, and we want to be able to change their quantities. We can write a sketch as follows, below is a component that renders a list with the names of our items and their quantities:


function ItemsList() {
    const bananas = 0;
    const fishes = 0;
    return (
        <ul>
            <li>`Bananas: ${ bananas }`</li>
            <li>`Fishes: ${ fishes }`</li>
        </ul>
    );
}

export default ItemsList;
Enter fullscreen mode Exit fullscreen mode

Okay, but this component is not yet dynamic. We have a pretty easy way to solve this by simply using a native hook from React.js, useState, which returns an array where the first element is the value and the second is the function to overwrite it, [value, setValue].


function ItemsList() {
    const [bananas, setBananas] = useState<number>(0);
    const [fishes, setFishes] = useState<number>(0);
    return (
        <ul>
            <li>`Bananas: ${ bananas }`</li>
            <li>`Fishes: ${ fishes }`</li>
        </ul>
    );
}

export default ItemsList;
Enter fullscreen mode Exit fullscreen mode

Great, now we can start manipulating the values of our items through their states. And for that, we need some helper functions and buttons.


function ItemsList() {
    const [bananas, setBananas] = useState<number>(0);
    const [fishes, setFishes] = useState<number>(0);

    const decreaseBananas = () => setBananas((prev) => prev - 1);
    const increaseBananas = () => setBananas((prev) => prev + 1);

    const decreaseFishes = () => setFishes((prev) => prev - 1);
    const increaseFishes = () => setFishes((prev) => prev + 1);

    return (
        <ul>
            <li>
                Bananas:
                <button onClick={ decreaseBananas }>-</button>
                <span>{ bananas }</span>
                <button onClick={ increaseBananas }>+</button>
            </li>
            <li>
            Fishes:
                <button onClick={ decreaseFishes }>-</button>
                <span>{ fishes }</span>
                <button onClick={ increaseFishes }>+</button>
            </li>
        </ul>
    );
}

export default ItemsList;
Enter fullscreen mode Exit fullscreen mode

I know this code looks messy, but it's for didactic purposes. Maybe you can already see the direction our code is taking, and surely you could do something cleaner and more interesting without the need of custom hooks.

Still, what's the big problem with this approach? Well, for each item we add, our code gains 3 more lines, which would be identical if it weren't for the different names. Also, for example, if now we needed to prevent the number from being negative like -1 or -2, we would have to change it in several lines.

That's exactly where we're going to recycle this logic with the help of custom hooks.

Solving the problem:

Before we build our custom hook, I'll point out a few details:

  • By convention, every hook should start with the lowercase word use;
  • Custom hooks are functions that contain other hooks inside;
  • Hooks should return some useful values, such as an initialized state and/or functions that alter it;

Now that we have that in mind, let's start building it.

function useItemCounter() {
    // initializing our state
    const [quantity, setQuantity] = useState<number>(0);

    // Our logic to increment the value by 1
    const increase = () => {
      setQuantity((prev) => prev + 1);
    };

    // Our logic to decrement the value by 1
    const decrease = () => {
      if(quantity - 1 < 0) return;
      setQuantity((prev) => prev -1);
    };

    return [quantity, increase, decrease];
}
Enter fullscreen mode Exit fullscreen mode

Now we're talking! This custom hook gives us control over all the actions and information we need. Let me explain step by step.

First, we initialize a state that will be responsible for storing our value and triggering the component re-render every time it's updated.

Then, we create functions that will work as specific actions to control our state.

Finally, we return the values we need; in this case, the two functions that control the state and the state itself.

Now we just need to use it in our example


function ItemsList() {
    const [bananas, increaseBananas,
    decreaseBananas] = useItemCounter();

    const [fishes, increaseFishes,
    decreaseFishes] = useItemCounter();

    return (
        <ul>
            <li>
                Bananas:
                <button onClick={ decreaseBananas }>-</button>
                <span>{ bananas }</span>
                <button onClick={ increaseBananas }>+</button>
            </li>
            <li>
            Fishes:
                <button onClick={ decreaseFishes }>-</button>
                <span>{ fishes }</span>
                <button onClick={ increaseFishes }>+</button>
            </li>
        </ul>
    );
}

export default ItemsList;
Enter fullscreen mode Exit fullscreen mode

Applying in the real world

Now that we're familiar with hooks, what they eat, and how they're born. Let's see some of the most common ones!

useLocalStorage


function useLocalStorage<T>(key: string, initialValue: T) {
    const readValue = () => {
    if(window === undefined) return initialValue;
    const savedValue = JSON.parse(localStorage.getItem(key));
    return savedValue || initialValue;
    }

    const [value, setValue] = useState<T>(readValue());

    useEffect(() => {
        localStorage.setItem(key, JSON.stringify(value))
    }, [value]);

    return [value, setValue];
}
Enter fullscreen mode Exit fullscreen mode

This hook works very similarly to a useState, since it simply returns the value and the function to overwrite it, with the small difference that if there's a value saved in the local storage key, it will be loaded on state initialization, and every time it's updated, the new value is saved in the local storage.

useFetch

function useFetch(url: string) {
    const [loading, setLoading] = useState<boolean>(true);
    const [error, setError] = useState<string>('');
    const [data, setData] = useState<any>();



    useEffect(() => {
        const fetchData = async () => {
            try {
                setLoading(true);
                setError('');
                const response = await fetch(url);
                const returnedData = await response.json();
                setData(returnedData);
            }
            catch(err) {
                if(err instanceof Error) {
                    setError(err?.message ?? 'Error fetching data');
                }
            }
            finally {
                setLoading(false);
            }
        };
        fetchData();
    }, []);

    return [data, loading, error]
}
Enter fullscreen mode Exit fullscreen mode

The useFetch is a way to perform that API fetch which often makes the code lengthy, and it already returns the loading status, error, and the data returned by the API.

useForm

function useForm<T>(initialForm: T) {
    const [form, setForm] = useState<T>(initialForm);

    const handleChange = ({ target }) => {
        setForm({ ...form, [target.name]: target.value});
    }

    const resetForm = () => setForm(initialForm);

    return [form, handleChange, resetForm];
}
Enter fullscreen mode Exit fullscreen mode

Better than creating a state for each form element is to unify them all into a small hook. Just pass an object as a parameter and your form control is done.

function LoginForm() {
    const initialForm = {email: '', password: ''};
    const [form, handleChange, resetForm] = useForm(initialForm);

    return (
        <form>
            <input
                type="text"
                name="email"
                value={ form.email }
                onChange={ (e) => handleChange(e) }
            />
            <input
                type="password"
                name="password"
                value={ form.password }
                onChange={ (e) => handleChange(e) }
            />
        </form>
    )
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Custom hooks are a design pattern in React.js that allow us to share logic between components, making the code cleaner and more reusable.

Top comments (1)

Collapse
 
vitorsales profile image
Vitor Sales

Well done, Vinicius. It made my understanding about this subject more clear