Mastering React Custom Hooks
PermalinkWhat are Custom Hooks?
Hooks were added in React 16.8 to separate and reuse component logic, including state and side effects.
Custom hooks are functions that allow you to reuse stateful logic between different React components. They are a way to extract component-specific logic into reusable functions, which can be shared between multiple components. Custom hooks typically start with the word "use" and allow you to use the state and other React features inside a functional component.
For example, you could create a custom hook useFormInput
that manages the state of a form input field, and use that hook in multiple components to handle form input fields in a consistent way. This allows you to avoid duplicating logic and keeps your code organized and easy to maintain.
PermalinkCreate your custom hook
To create a custom hook you need to define a function which starts with the word "use" (E.g. useState, useFormInput). We will create useFormInput
as an example.
The useFormInput
hook will manage the state of our <form />
element.
/*
* useFormInput hook to manage state of form
* @param {object} defaultValue - takes default state of form elements
*/
const useFormInput = (defaultValue) => {
const [data, setData] = useState(defaultValue || {});
const setFormData = useCallback(
(key, value) => {
setData({ ...data, [key]: value });
},
[setData, data]
);
const memoizedValue = useMemo(
() => ([
data,
setFormData,
]),
[data, setFormData]
);
return memoizedValue;
};
export default useFormInput;
useFormInput
hook usage in a top-level component.
// Top level App component
const App = () => {
// Usage of useFormInput hook
const [formData, setFormData] = useFormInput({
email: "",
password: ""
});
const submitForm = (e) => {
e.preventDefault();
console.log("form submitted");
console.log("submitted data:", formData);
}
return (
<>
<form onSubmit={submitForm}>
<input
id="email"
name="email"
defaultValue={formData.email}
onChange={e => {
setFormData("email", e.target.value);
}}
/>
<input
id="password"
name="password"
defaultValue={formData.password}
onChange={e => {
setFormData("password", e.target.value);
}}
/>
<button type="submit">Login</button>
</form>
</>
);
};
export default App;
Now that we know how custom hooks are created, let's take a look at some useful custom hooks.
PermalinkUseful Hooks
PermalinkuseBreakpoint
useBreakpoint
hook returns a boolean value for a specific predefined breakpoint. This can be used to apply classes or styles responsively.
The hook also takes an optional target element and custom breakpoints to observe element resize. By default, it observes <body />
element of the HTML document.
const defaultBreakpoints: Record<string, number> = {
base: 0,
xs: 480,
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
"2xl": 1536,
};
const useBreakpoint = (
bp: string,
target?: Element,
breakpoints: Record<string, number> = defaultBreakpoints
) => {
const [state, setState] = useState(false);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (Object.keys(breakpoints).indexOf(bp) < 0)
throw new Error("Unknown breakpoint");
const observer = new ResizeObserver(([entry]) => {
const wd = entry.contentRect.width;
setState(wd >= breakpoints[bp]);
});
observer.observe(target || document.body);
setLoaded(true);
return () => observer.unobserve(target || document.body);
}, [bp, target, breakpoints, setState]);
return !loaded ? undefined : state;
};
export default useBreakpoint;
// Example usage - const sm = useBreakpoint("sm");
PermalinkuseScroll
useScroll
hook returns vertical and horizontal scroll offset. The hook can optionally take a target element. This can be used to target the scroll of a specific element. The hook by default targets the window scroll event.
const useScroll = (ele?: Element) => {
const [offset, setOffset] = useState({x: 0, y: 0});
useEffect(() => {
function handleScroll() {
const [x, y] = ele
? [ele.scrollTop, ele.scrollLeft]
: [window.scrollX, window.scrollY];
setOffset({ x, y });
}
window.addEventListener("scroll", handleScroll, {passive: true});
return () => window.removeEventListener("scroll", handleScroll);
}, [ele]);
return offset;
};
export default useScroll;
// Example usage - const {x, y} = useScroll()
PermalinkuseToast
useToast
hook uses the Provider pattern and can only be used inside the Provider boundary. In our case, we will be using Context API to implement a ToastBoundary
inside which, the hook can be used.
The top-level component must be wrapped in ToastBoundary
before we use useToast
hook. The hook takes an object with id
, content
and timeout
of the toast.
type ToastContextType = {
show: boolean;
setShow: (v: boolean) => void;
setToast: (v: {
id: string | null;
content: string | ReactNode | null;
}) => void;
}
type ToastType = {
id: string | null;
content: string | ReactNode | null;
}
const ToastContext = createContext<ToastContextType>({
show: false,
setShow: (v) => {},
setToast: (v) => {},
});
export const ToastBoundary: FC<{ children: ReactNode }> = ({ children }) => {
const [show, setShow] = useState<boolean>(false);
const [toast, setToast] = useState<ToastType>({
id: null,
content: null
});
const memoizedVals = useMemo(
() => ({ show, setShow, setToast }),
[show]
);
return (
<ToastContext.Provider value={memoizedVals}>
{children}
<div id={toast.id ? toast.id + "_container" : undefined} className="toast-container">
<div id={toast.id ?? undefined} className="toast">
{toast.content}
</div>
</div>
</ToastContext.Provider>
);
};
const useToast = ({
id,
content,
timeout = 2000,
}: {
id: string | null;
content: string | ReactNode | null;
timeout?: number;
}) => {
const { show, setToast, setShow } = useContext(ToastContext);
const memoizedState = useMemo(
() => ({
show: () => setShow(true),
hide: () => setShow(false),
}),
[setShow]
);
useEffect(() => {
const timer = setTimeout(() => {
if (show) setShow(false);
}, timeout);
return () => clearTimeout(timer);
}, [show, setShow, timeout]);
useEffect(() => {
setToast({ id, content });
}, [setToast, id, content]);
return memoizedState;
};
export default useToast;
/*
Example usage -
~ In Top level component
...
return(
<ToastBoundary>
<App />
</ToastBoundary>
);
~ In component file
const toast = useToast({
id: "example_toast",
content: "Example Toast Content"
});
*/
PermalinkConclusion
Hopefully, by now you have a better understanding of React custom hooks and the hooks that are given in this article might come in handy in your development journey.
Comment in case you need a part 2 of this.
Cheers ๐๐