This page looks best with JavaScript enabled

Five React hooks useful in any project.

 ·  ☕ 8 min read  ·  ✍️ Iskander Samatov

react hooks



 In this post, I will cover five simple React hooks that you will find handy in any project. These hooks are useful no matter the features of the application. For each hook, I will provide the implementation and the client code sample.

useModalState

Web applications use modals extensively and for various reasons. When working with modals, you quickly realize that managing their state is a tedious and repetitive task. And when you have code that’s repetitive and tedious, you should take time to abstract it. That’s what useModalState does for managing modal states.

Many libraries provide their version of this hook, and one such library is Chakra UI. If you want to learn more about Chakra UI, check out my blog post here.

The implementation of the hook is very simple, even trivial. But in my experience, it pays off using it rather than rewriting the code for managing the modal’s state each time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import React from "react";
import Modal from "./Modal";

export const useModalState = ({ initialOpen = false } = {}) => {
  const [isOpen, setIsOpen] = useState(initialOpen);

  const onOpen = () => {
    setIsOpen(true);
  };

  const onClose = () => {
    setIsOpen(false);
  };

  const onToggle = () => {
    setIsOpen(!isOpen);
  };

  return { onOpen, onClose, isOpen, onToggle };
};

And here’s an example of client code using the hook:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const Client = () => {
  const { isOpen, onToggle } = useModalState();

  const handleClick = () => {
    onToggle();
  };

  return (
    <div>
      <button onClick={handleClick} />
      <Modal open={isOpen} />
    </div>
  );
};

export default Client;

useConfirmationDialog

useConfirmationDialog is another modal-related hook that I use quite often. It’s a common practice to ask users for confirmations when performing sensitive actions, like deleting records. So it makes sense to abstract that logic with a hook. Here’s a sample implementation of the useConfirmationDialog** **hook:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { useCallback, useState } from 'react';
import ConfirmationDialog from 'components/global/ConfirmationDialog';

export default function useConfirmationDialog({
    headerText,
    bodyText,
    confirmationButtonText,
    onConfirmClick,
}) {
    const [isOpen, setIsOpen] = useState(false);

    const onOpen = () => {
        setIsOpen(true);
    };

    const Dialog = useCallback(
        () => (
            <ConfirmationDialog
                headerText={headerText}
                bodyText={bodyText}
                isOpen={isOpen}
                onConfirmClick={onConfirmClick}
                onCancelClick={() => setIsOpen(false)}
                confirmationButtonText={confirmationButtonText}
            />
        ),
        [isOpen]
    );

    return {
        Dialog,
        onOpen,
    };
}

And here’s an example of the client code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React from "react";
import { useConfirmationDialog } from './useConfirmationDialog'

function Client() {
  const { Dialog, onOpen } = useConfirmationDialog({
    headerText: "Delete this record?",
    bodyText:
      "Are you sure you want delete this record? This cannot be undone.",
    confirmationButtonText: "Delete",
    onConfirmClick: handleDeleteConfirm,
  });

  function handleDeleteConfirm() {
    //TODO: delete
  }

  const handleDeleteClick = () => {
    onOpen();
  };

  return (
    <div>
      <Dialog />
      <button onClick={handleDeleteClick} />
    </div>
  );
}

export default Client;

One thing to note here is that this implementation works fine as long as your confirmation modal doesn’t have any controlled input elements. If you do have controlled inputs, it’s best to create a separate component for your modal. That’s because you don’t want the content of the modal, including those inputs, to re-render each time the user types something.

useAsync

Properly handling async actions in your application is trickier than it seems at first. There are multiple state variables that you need to keep track of while the task is running. You want to keep the user informed that the action is processing by displaying a spinner. Also, you need to handle the errors and provide useful feedback. So it pays off to have an established framework for dealing with async tasks in your React project. And that’s where you might find useAsync useful. Here’s an implementation of the useAsync hook:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export const useAsync = ({ asyncFunction }) => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [result, setResult] = useState(null);

  const execute = useCallback(
    async (...params) => {
      try {
        setLoading(true);
        const response = await asyncFunction(...params);
        setResult(response);
      } catch (e) {
        setError(e);
      }
      setLoading(false);
    },
    [asyncFunction]
  );

  return { error, result, loading, execute };
};

The client code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from "react";

export default function Client() {
  const { loading, result, error, execute } = useAsync({
    asyncFunction: someAsyncTask,
  });

  async function someAsyncTask() {
    // perform async task
  }

  const handleClick = () => {
    execute();
  };

  return (
    <div>
      {loading && <p>loading</p>}
      {!loading && result && <p>{result}</p>}
      {!loading && error?.message && <p>{error?.message}</p>}
      <button onClick={handleClick} />
    </div>
  );
}

The hook is not hard to write yourself and that’s what I often do. But it might make sense for you to use a more mature library implementation instead. Here’s a great option.

useTrackErrors

Form validation is another part of React applications that people often find tedious. With that said, there are plenty of great libraries to help with forms management in React. One great alternative is formik. However, each of those libraries has a learning curve. And that learning curve often makes it not worth using in smaller projects. Particularly if you have others working with you and they are not familiar with those libraries.

But it doesn’t mean we can’t have simple abstractions for some of the code we often use. One such piece of code that I like to abstract is error validation. Checking forms before submitting to API and displaying validation results to the user is a must-have for any web application. Here’s an implementation of a simple useTrackErrors hook that can help with that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState } from "react";
import FormControl from "./FormControl";
import Input from "./Input";
import onSignup from "./SignupAPI";

export const useTrackErrors = () => {
  const [errors, setErrors] = useState({});

  const setErrors = (errsArray) => {
    const newErrors = { ...errors };
    errsArray.forEach(({ key, value }) => {
      newErrors[key] = value;
    });

    setErrors(newErrors);
  };

  const clearErrors = () => {
    setErrors({});
  };

  return { errors, setErrors, clearErrors };
};

And here’s the client implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import React, { useState } from "react";
import FormControl from "./FormControl";
import Input from "./Input";
import onSignup from "./SignupAPI";

 
export default function Client() {
  const { errors, setErrors, clearErrors } = useTrackErrors();

  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const handleSignupClick = () => {
    let invalid = false;

    const errs = [];
    if (!name) {
      errs.push({ key: "name", value: true });
      invalid = true;
    }
    if (!email) {
      errs.push({ key: "email", value: true });
      invalid = true;
    }

    if (invalid) {
      setErrors(errs);
      return;
    }

    onSignup(name, email);
    clearErrors();
  };

  const handleNameChange = (e) => {
    setName(e.target.value);
    setErrors([{ key: "name", value: false }]);
  };

  const handleEmailChange = (e) => {
    setEmail(e.target.value);
    setErrors([{ key: "email", value: false }]);
  };

  return (
    <div>
      <FormControl isInvalid={errors["name"]}>
        <FormLabel>Full Name</FormLabel>
        <Input
          onKeyDown={handleKeyDown}
          onChange={handleNameChange}
          value={name}
          placeholder="Your name..."
        />
      </FormControl>
      <FormControl isInvalid={errors["email"]}>
        <FormLabel>Email</FormLabel>
        <Input
          onKeyDown={handleKeyDown}
          onChange={handleEmailChange}
          value={email}
          placeholder="Your email..."
        />
      </FormControl>
      <button onClick={handleSignupClick}>Sign Up</button>
    </div>
  );
}

useDebounce

Debouncing has a broad use in any application. The most common use is throttling expensive operations. For example, preventing the application from calling the search API every time the user presses a key and letting the user finish before calling it. The useDebounce hook makes throttling such expensive operations easy. Here’s a simple implementation written using AwesomeDebounceLibrary under the hood:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import AwesomeDebouncePromise from "awesome-debounce-promise";


const debounceAction = (actionFunc, delay) =>
  AwesomeDebouncePromise(actionFunc, delay);

function useDebounce(func, delay) {
  const debouncedFunction = useMemo(() => debounceAction(func, delay), [
    delay,
    func,
  ]);

  return debouncedFunction;
}

And here’s the client code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import React from "react";

const callAPI = async (value) => {
  // expensice API call
};

export default function Client() {
  const debouncedAPICall = useDebounce(callAPI, 500);

  const handleInputChange = async (e) => {
    debouncedAPICall(e.target.value);
  };

  return (
    <form>
      <input type="text" onChange={handleInputChange} />
    </form>
  );
}

One thing to note with this implementation: You need to ensure that the expensive function is not recreated on each render. Because that will reset the debounced version of that function and wipe out its inner state. There are two ways to achieve that:

  1. Declare the expensive function outside of the functional component (like in the code example).
  2. Wrap the expensive function with useCallback hook.

And that’s it for this post. As a side note, there are many useful hook libraries worth checking out, and if you’re interested, here’s a great place to start. While there are many helpful custom hooks out there, these five are the ones that you will find handy in any React project.

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.