React has undergone significant modifications in the previous three years, resulting in significant changes in the way React apps are built. The most significant modifications are the Functional Component Pattern and Hooks. Hooks are the newest trend in the React community and are inevitable for any React Developer
In this article, you will be able to create custom hooks that can be frequently used and can cut down on a lot of boilerplate code. Before that, let's look into what exactly are hooks.
React Hooks was first suggested in October 2018 and then published four months later in February 2019. Hooks have been swiftly learned and adopted in production codebases since then because they greatly simplify the handling of state and side effects in applications.
Before React 16.8, implementing states and life cycle methods in a functional component was not possible. The introduction of hooks made this possible. Hooks are functions that let you “hook into” React state and lifecycle features from functional components. Following are a few inbuilt hooks provided by React.
These are a few of the inbuilt hooks, React has more to offer. For in-depth details about these hooks, you can check the official documentation. For video enthusiasts, you can check out the hooks playlist by Ben Awad.
As projects develop, the likelihood of a specific piece of code recurring increases dramatically. In such cases, it's always a good idea to abstract the logic of these components into bespoke utils. Custom hooks are used in frontend react apps when state and life cycle methods are required to implement the abstraction logic.
Before we look into some of the useful custom hooks, it is necessary that you understand why hooks are really important and also understand a few rules related to hooks while using them.
You might have heard of Kent C. Dodds, he is a big deal in React and his blog is one of the most revered blogs out there. Here is a direct quote from him regarding hooks.
Hooks come with a lot of benefits, but one of my favourites is that it makes your components more declarative in that it allows you to stop thinking about "when things should happen in the lifecycle of the component" (which doesn't matter all that much) and more about "when things should happen in relation to state changes" (which matters much more).
In short, what he means to say is, lifecycle methods should occur as per the changes in the state, and as we think about it, it does make sense. And hence understanding hooks is a big step in the journey of learning React.
With this said, here are a few rules that one needs to follow while using hooks. These are important and would be needed as we go on building our own custom hooks.
Call hooks only on top-level:
Hooks cannot be called inside conditionals, loops or nested functions in a component. Make sure you're using the hook at the top level of your functional components at all times.
Call hooks from React functions:
Hooks should be called from the React functional component, or from our custom hooks. Hooks should never be called from a javascript function.
Order of hooks:
React relies on the order in which hooks are called, so make sure they're in the right order as per your functionality/requirement.
With this said, let's look into a few custom hooks that I use in my projects to make things easier for me.
useInput
import {useState} from 'react';export const useInput = initialState => {const [state, setState] = useState(initialState);const set = event => setState(event.target.value);const reset = () => setState(initialState);return [state, set, reset];};
import React from 'react';// Utilsimport {useInput} from '../hooks';const App = () => {const [name, setName] = useInput();const [email, setEmail] = useInput();return (<div><input value={name} onChange={setName} /><input value={email} onChange={setEmail} /></div>);};export default App;
useSwitch and useToggle
import {useState} from 'react';export const useSwitch = (initialState = false) => {const [state, setState] = useState(initialState);const open = () => setState(true);const close = () => setState(false);return [state, open, close];};export const useToggle = (initialState = false) => {const [state, setState] = useState(initialState);const toggle = () => setState(currentState => !currentState);return [state, toggle, setState];};
useAsync
import {useEffect, useState} from 'react';/*options: {executeOnMount: boolean,dependencies: array}*/const defaultOptions = {executeOnMount: false,dependencies: [],};export const useAsync = (api, options = defaultOptions) => {const [data, setData] = useState();const [error, setError] = useState(null);const [waiting, setWaiting] = useState(true);if (!api || typeof api !== 'function') {const message = `API not valid!`;throw new Error(message);}const onData = d => {setWaiting(false);setError(null);setData(d);};const onError = err => {setWaiting(false);setError(err);};const execute = async () => {if (!waiting) {setWaiting(true);try {const d = await api();onData(d);} catch (err) {onError(err);}}};useEffect(() => {if (options?.executeOnMount) execute();}, [...options?.dependencies]);return [data, error, waiting, execute];};
import React from 'react';// Librariesimport axios from 'axios';// Utilsimport {useAsync} from '../hooks';const App = () => {const fetchData = axios.get('https://api.yourdomain.com/users');const [data, error, waiting] = useAsync(fetchData, {executeOnMount: true});if (waiting) {return <h1>Loading...</h1>;}return <div>{error && !data ? <h1>{error}</h1> : <h1>{data}</h1>}</div>;};export default App;
useEventListener
import {useEffect, useRef} from 'react';export const useEventListener = (eventName, handler, element = window) => {const eventHandler = useRef();useEffect(() => {eventHandler.current = handler;}, [handler]);useEffect(() => {if (!element && !element.addEventListener) return;element.addEventListener(eventName, event =>eventHandler.current(event),);return () => element.removeEventListener(eventName, eventListener);}, [eventName, element]);};
import React from 'react';// Utilsimport {useEventListener} from '../hooks';const App = () => {const element = document.querySelector('#button-id');const [showDate, setShowDate] = useState();const eventHandler = () => setShowDate(current => !current);useEventListener('click', handler, element);return (<div><button id="#button-id">Click here for date</button>{showDate && <h1>{new Date().toDateString()}</h1>}</div>);};export default App;
useIsComponentUnmounted
import {useRef, useEffect} from 'react';export const useIsComponentUnmounted = () => {const isMounted = useRef(true);useEffect(() => {return () => (isMounted.current = false);}, []);return isMounted.current;};
import React, {useEffect, useState} from 'react';// Librariesimport axios from 'axios';// Utilsimport {useIsComponentUnmounted} from './hooks';const App = () => {const [name, setName] = useState();const isUnmounted = useIsComponentUnmounted();useEffect(() => {if (!isUnmounted) {axios.get('https://api.yourdomain.com/getName').then(data => setName(data.name)).catch(error => console.log(error));}}, []);return <h1>Hello {name}</h1>;};
useScript
import {useEffect, useState} from 'react';/*constants in order to maintain consistency*/const STATUS = {IDLE: 'Idle',LOADING: 'Loading scripts',READY: 'Script loaded successfully',ERROR: 'Unable to load Script',};export const useScript = src => {const [status, setStatus] = useState(src ? STATUS.LOADING : STATUS.IDLE);// Helper function to add listenersconst addEventListeners = (element, onChange) => {element.addEventListener('load', onChange);element.addEventListener('error', onChange);};useEffect(() => {// Allow falsy src value if waiting on other data needed for// constructing the script URL passed to this hook.if (!src) {setStatus(STATUS.ERROR);return;}// Fetch existing script element by src// It may have been added by another intance of this hooklet script = document.querySelector(`script[src="${src}"]`);if (!script) {// Create scriptscript = document.createElement('script');script.src = src;script.async = true;script.setAttribute('data-status', 'loading');// Add script to document bodydocument.body.appendChild(script);// Store status in attribute on script// This can be read by other instances of this hookconst setAttributeFromEvent = event => {script.setAttribute('data-status',event.type === 'load' ? 'ready' : 'error',);};addEventListeners(script, setAttibuteFromEvent);} else {// Grab existing script status from attribute and set to state.setStatus(STATUS.LOADING);}// Script event handler to update status in state// Note: Even if the script already exists we still need to add// event handlers to update the state for *this* hook instance.const setStateFromEvent = event => {setStatus(event.type === 'load' ? STATUS.READY : STATUS.ERROR);};// Add event listenersaddEventListeners(script, setStateFromEvent);// Remove event listeners on cleanupreturn () => {if (script) {script.removeEventListener('load', setStateFromEvent);script.removeEventListener('error', setStateFromEvent);}};}, [src]);return status;};
useMediaQuery
import {useState, useEffect} from 'react';export const useMediaQuery = query => {const [matches, setMatches] = useState(false);useEffect(() => {const media = window.matchMedia(query);if (media.matches !== matches) {setMatches(media.matches);}const listener = () => setMatches(media.matches);window.addEventListener('resize', listener);return () => window.removeEventListener('resize', listener);}, [matches, query]);return matches;};
import React from 'react';import {useMediaQuery} from './hooks';function App() {// You can use any @media propertyconst isDesktop = useMediaQuery('(min-width: 960px)');return (<div className="App">{isDesktop ? <h1>Desktop</h1> : <h1>Mobile</h1>}</div>);}
useLocalStorage
export const useLocalStorage = (key, initialValue) => {const [error, setError] = useState(null);// State to store our value// Pass initial state function to useState so logic is only executed onceconst [storedValue, setStoredValue] = useState(() => {try {// Get from local storage by key and// parse stored json or if none return initialValueconst item = window.localStorage.getItem(key);return item ? JSON.parse(item) : initialValue;} catch (error) {// If error also return initialValuesetError({type: 'Initialisation', message: error});return initialValue;}});// ... persists the new value to localStorage.const setValue = value => {try {// Save to local state and to local storagesetStoredValue(value);window.localStorage.setItem(key, JSON.stringify(value));} catch (error) {setError({type: 'Set Value', message: error});}};// ... remove the persisted value from localStorageconst removeValue = () => {try {setStoredValue(null);window.localStorage.removeItem(key);} catch (error) {setError({type: 'Remove Value', message: error});}};return [storedValue, setValue, removeValue, error];};
Okay! So there you go, quite a few custom hooks for you to use and experiment around. Hooks are extremely powerful and when done right can help you to write clean and concise code. Saying this, there are still quite a few things that you can look into. With these examples, I hope you were able to get an idea about how to create custom hooks and I hope that you would use these hooks in your own projects as well.
If you have any questions or suggestions or any other custom hooks, feel free to write in the comments below.