DEV Community

Cover image for Writing your 1st Custom Hook
Ethan Soo Hon
Ethan Soo Hon

Posted on

Writing your 1st Custom Hook

We've all heard of "custom hooks" before but many people still learning React find them intimidating (speaking from personal experience!) and it's hard to see the benefit without a concrete example.

Confused Gif

In this tutorial we will first attempt to solve a problem without using a custom hook then we will refactor the code to use a custom hook and see how much cleaner and how much less code there actually is.

Semantic UI

For this tutorial we will use the react component library Semantic UI. No particular reason, I have just been using it lately and it is relatively simple. After running create-react-app these are our only 2 dependencies

$  yarn add semantic-ui-react semantic-ui-css
## Or NPM
$  npm install semantic-ui-react semantic-ui-css
Enter fullscreen mode Exit fullscreen mode

To quickly bootstrap our project so we can get on with writing our custom hook let's grab a layout example from their site.

![Example screen](https://i.gyazo.com/b3bd5a64368f11e469ea481c0eba3913.png) *[Link to page](https://react.semantic-ui.com/layouts/login)*

This resulting layout is achieved with Semantic UI components in about 30 lines. We will take this code and make a Login.js component.

We'll place our Login in our App.js and don't forget the import line for the minified stylesheet or else the library won't work!

import './App.css';
import Login from './Login.js';
import 'semantic-ui-css/semantic.min.css'

function App() {
  return (
    <main>
      <Login></Login>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Goal

We'll try to keep the problem as simple as possible; we want an alert/message to be transitioned in at the top of the screen when a user's login fails or succeeds. In addition, we want for that message to timeout after a specific amount of time. This form is not connected to anything so to emulate a login failing (by network issues, incorrect credentials etc...) or succeeding we use Math.random() each time the form is submitted.

/* Inside Login.js*/
<Form
   size='large'
   onSubmit={(e) => {
      e.preventDefault();
      const RANDOM_FAILURE = Math.random();
      if (RANDOM_FAILURE >= 0.5) {
         console.log('Login Succeeded');
      } else {
        console.log('Login failed');                
       }
   }}
   >
Enter fullscreen mode Exit fullscreen mode

Message Component + API

Semantic UI comes with a Message component that looks like the perfect candidate for a pop-up message.

![Message Component](https://i.gyazo.com/da35a83a85a17ba213e7f227d97497cf.png) *[Link to page](https://react.semantic-ui.com/collections/message/#types-message)*

Looking at the props we can see that the Message component needs the following props in order to be customized...
Note: As seen below some of the props here do the same thing so we only need one of them.

![Message Component](https://i.gyazo.com/1057565bdb6511fdbe6cd5c178ec80d3.png) *Redundant props success & positive, negative & error and hidden and visible*

Transition Component + API

We also want to transition the message in smoothly with a CSS animation. We could do it manually but Semantic UI also comes with a Transition component that does it for us if we wrap around our it around our Message. This way we can specify the animation we want as a string prop. Here's a basic example

   <Transition
        duration={2000}
        animation="scale"
        visible={false}
        unmountOnHide={true}>
     <p> Hello World! </p>
</Transition>
Enter fullscreen mode Exit fullscreen mode

The transition component will only trigger when its visible prop is changed from false to true or vice-versa. You Cannot just do visible={true} and expect it to work.

Attempt 1

First let's make our local state for the message and transition. We will set the Message component as always visible (props visible={true} and hidden={false}) and let the Transition component wrapped around it handle the visibility. We then create our piece of state (using an object) and fill it will all the info that we need to customize our Message via props.


  const [messageConfig, setmessageConfig] = useState({
    visible: false,
    header: "",
    content: "",
    error: false,
    success: false,
  });
Enter fullscreen mode Exit fullscreen mode

Okay nows let's pass the properties from this state object into our Components! Notice in the onDismiss for our Message (a required prop) we just hide the message and set the state back to the default values.

            <Transition
                duration={2000}
                animation='scale'
                visible={messageConfig.visible}
                unmountOnHide={true}
            >
                <Message
                    onDismiss={() => {
                    setmessageConfig({
                            header: '',
                            content: '',
                            error: false,
                            success: false,
                            visible: false
                        });
                    }}
                    compact
                    size='large'
                    content={messageConfig.content}
                    header={messageConfig.header}
                    error={messageConfig.error}
                    success={messageConfig.success}
                    visible={true}
                    hidden={false}
                >

                </Message>
            </Transition>
Enter fullscreen mode Exit fullscreen mode

Now that we have everything setup we just need to call our setMessageConfig whenever we want to see a message and then create a setTimeout() to hide the message after a specific period of time (let's say 2s).

Here's our onSubmit from before now with the newly added code

    onSubmit={(e) => {
                            e.preventDefault();
                            const RANDOM_FAILURE = Math.random();
                            console.log(RANDOM_FAILURE);
                            if (RANDOM_FAILURE >= 0.5) {
                                console.log('Login Succeeded');
                                setmessageConfig({
                                    header: 'Success',
                                    content: 'Login Successfull',
                                    error: false,
                                    success: true,
                                    visible: true
                                });
                                setTimeout(() => {
                                    setmessageConfig({
                                        header: '',
                                        content: '',
                                        error: false,
                                        success: true,
                                        visible: false
                                    });
                                }, 2000);
                            } else {
                                console.log('Login failed');
                                setmessageConfig({
                                    header: 'Failure',
                                    content: 'Login Failed',
                                    error: false,
                                    success: true,
                                    visible: true
                                });
                                setTimeout(() => {
                                    setmessageConfig({
                                        header: '',
                                        content: '',
                                        error: false,
                                        success: true,
                                        visible: false
                                    });
                                }, 2000);
                            }
                        }}
Enter fullscreen mode Exit fullscreen mode
![Working Message](https://i.gyazo.com/e97fe65c33c2e87e9207db775fac6bd6.png) *Technically it works... but we can do better*

It works... but look at all the code we have to type everytime we need to show and autoHide a message. Let's try to refactor it into a custom hook.

Custom Hook rules

These are paraphrased directly from the React docs

1) The code written by the custom hook will be functionally equivalent to what we have now. The advantage of refactoring is having a cleaner, easier to understand code base and reusable logic.

2) Name your custom hooks function by the React convention (useSomething). Is it required? No. But it is a very important convention for others you will potentially share the hook/codebase with.

3) Components using the same custom hook do not share state variables; the state variables defined in custom hooks are fully isolated.

Writing the hook

A custom hook is just a function; the thing that makes it a custom hook is its use of built-in React hooks functions (useState, useEffect etc...) and reusable logic. I defined an object to hold the initial state of our message just for convenience.

import { useState } from 'react';
const INITIAL_STATE = {
    header: '',
    content: '',
    error: false,
    success: false,
    visible: false
};
function useMessage(autoHideDuration = 2000) {
    const [messageConfig, setmessageConfig] = useState(INITIAL_STATE);
    function showMessage(config = { ...INITIAL_STATE }) {
        setmessageConfig({ ...config, visible: true });
        setTimeout(() => {
            hideMessage();
        }, autoHideDuration );
    }
    function hideMessage() {
        setmessageConfig({...INITIAL_STATE});
    }
    return [showMessage, hideMessage, messageConfig];
}

Enter fullscreen mode Exit fullscreen mode

All we did here was

  • Name our function according to hook rules. We take in an argument here to determine the delay before the message hides. Your custom hook can take in arguments or not, up to you.
  • Take the defined state object in Login.js and move it in here.
  • Create two helper functions to manipulate the state so when we are using the hook we don't have call the setter or the setTimeout directly making it cleaner and easier to use.
  • showMessage takes in the config from the user, sets visible to true, and then initiates a setTimeout that hides the Message after the period is over.
  • There is no strict rule that states what should be returned from a custom hook needs to be in an array, but most people follow the return pattern used by useState and return an array of multiple values we can destructure to access.

Using the custom hook

We removed the state messageConfig from Login.js and replaced it with our custom hook.

const [showMessage, hideMessage, messageConfig] = useMessage(2500);
Enter fullscreen mode Exit fullscreen mode

Here's how our onSubmit now looks!

onSubmit={(e) => {                           
  e.preventDefault();                            
  const RANDOM_FAILURE = Math.random();                          
  if (RANDOM_FAILURE >= 0.5) {                               
    showMessage({                                
       header: 'Success',                                
       content: 'Login Successful',                          
       error: false,                         
       success: true,
       visible: true                         
    });
  } else {                               
     showMessage({                               
       header: 'Failure',                                
       content: 'Login Failed',                              
       error: true,                              
       success: false,                               
      visible: true                          
    });
}
}}
Enter fullscreen mode Exit fullscreen mode
*Wayyyyy better* ๐Ÿ˜„

Here's the final version; have fun writing custom hooks!

*Cover image Credit : Link to Image

Top comments (0)