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.
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
To quickly bootstrap our project so we can get on with writing our custom hook let's grab a layout example from their site.
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>
);
}
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');
}
}}
>
Message Component + API
Semantic UI comes with a Message component that looks like the perfect candidate for a pop-up 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.
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>
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,
});
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>
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);
}
}}
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];
}
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);
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
});
}
}}
Here's the final version; have fun writing custom hooks!
*Cover image Credit : Link to Image
Top comments (0)