A small collection of simple React Hooks you're probably rewriting on a regular basis
npm install @figliolia/react-hooks1. useMount - Execute a callback on first mount
2. useUnmount - Execute a callback on unmount
3. useLoadingState - Provides a controller for getting/setting loading, error, and success states for a UI interaction
4. useFormState - Provides a controller with access to form data, loading, error, and success states for a form submission
5. useTimeout - Execute deferred callbacks that'll automatically clean themselves up when the hook unmounts
6. useAnimationFrame - Execute callbacks in requestAnimationFrame that'll automatically stop/cancel if the hook unmounts
6. useIdleCallback - Execute browser-idle callbacks in requestIdleCallback that'll automatically stop/cancel if the hook unmounts
7. useController - Maintains a stable reference to an object instance (created by a component) between renders
8. useDebouncer - Given a callback and a wait period, returns a debounced implementation of that callback. The scheduled callback will automatically cancel if the component unmounts.
9. useThrottler - Given a callback and a wait period, returns a throttled implementation of that callback
10. useLocale - Returns the user's specified locale and rerenders whenever it changes
11. useFocusedKeyListener - A hook that will respond to keydown events if target element comes into focus
12. useWindowSize - A hook that returns the current dimensions of the window object. When the window is undefined (in SSR environments), the height and width dimensions are set to zero
13. useNodeDimensions - A hook that returns a ref and a dimensions object containing width and height values. When the provided ref is attached to a DOM element, the hook will rerender a new dimensions object each time the element's width or height change.
14. useMergedRefs - A hook that accepts any number of ref callbacks or ref objects and returns a single ref callback. Useful for when managing multiple refs that need to a attach to a single component
bash
npm i @figliolia/react-hooks
or
yarn add @figliolia/react-hooks
`Basic Usage
$3
Execute a callback on the first mount of a hook or component
`tsx
import { useMount } from "@figliolia/react-hooks";export const MyComponent = () => {
useMount(() => {
// Executes on first mount!
});
return (
// JSX
);
}
`$3
Execute a callback when a hook or component unmounts - note - this callback will not run when props change
`tsx
import { useUnmount } from "@figliolia/react-hooks";export const MyComponent = () => {
useUnmount(() => {
// Executes on unmount!
});
return (
// JSX
);
}
`$3
Provides a controller for getting/setting loading, error, and success states for a UI interaction
`tsx
import { useLoadingState } from "@figliolia/react-hooks";
import { useClassNames } from "@figliolia/classnames";
import { useCallback } from "react";export const MyComponent = () => {
const { setState, resetState, ...state } = useLoadingState();
// state = { loading: boolean, error: boolean | string, success: boolean }
const onClick = useCallback(() => {
setState("loading", true);
void fetch("/api/some-data")
.then(data => data.json())
.then(data => {
setState("success", true);
}).catch(() => {
setState("error", true);
});
}, [setState, reset]);
const classes = useClassNames("my-button", state);
return (
className={classes}
onClick={onClick}>Click Me!
);
}
`$3
Provides a controller with access to form data, loading, error, and success states for a form submission
`tsx
import { useFormState } from "@figliolia/react-hooks";
import { useClassNames } from "@figliolia/classnames";export const MyForm = () => {
const {
error,
loading,
success,
onSubmit, // form submit handler
} = useFormState((data, setState) => {
// data = FormData({ name, email, password })
setState("loading", true);
void fetch("/post-data", {
body: data,
method: "POST",
}).then(() => {
setState("success", true);
}).catch(() => {
setState("error", true);
})
});
const classes = useClassNames("my-button", { error: !!error, success, loading });
return (
);
}
`$3
Execute deferred callbacks that'll automatically clean themselves up when the hook unmounts
`tsx
import { useState, useCallback } from "react";
import { useTimeout } from "@figliolia/react-hooks";export const CounterButton = () => {
const [count, setCount] = useState(0);
const timeout = useTimeout();
const onClick = useCallback(() => {
timeout.execute(() => {
setCount(count => count + 1);
}, 1000);
// If CounterButton unmounts, all pending calls to
// timeout.execute() are cancelled
}, []);
return (
);
}
`$3
Execute callbacks in requestAnimationFrame that'll automatically stop/cancel if the hook unmounts
`tsx
import { useAnimationFrame } from "@figliolia/react-hooks";export const ProgressIndicator = () => {
const [count, setCount] = useState(0);
const animator = useAnimationFrame();
const onClick = useCallback(() => {
animator.execute(() => {
setProgress(progress => progress + 1);
});
// If ProgressIndicator unmounts, all pending calls to
// animator.execute() are cancelled
}, []);
return (
<>
className="progress-bar"
style={{ width: ${progress}% }}>
>
);
}
`$3
Execute callbacks in requestIdleCallback that'll automatically stop/cancel if the hook unmounts. Please refer to this chart to decide whether or not polyfilling the API is necessary
`tsx
import { useIdleCallback } from "@figliolia/react-hooks";export const BackgroundTaskScheduler = ({ task }) => {
const [count, setCount] = useState(0);
const manager = useIdleCallback();
const scheduleTask = useCallback(() => {
manager.execute(() => {
task();
// do some background tasks
},
// optionally supply a task deadline
{ timeout: 3000 }
);
// If BackgroundTaskScheduler unmounts, all pending calls to
// manager.execute() are cancelled
}, [task]);
return (
);
}
`
$3
Maintains a stable reference to an object instance (created by a component) between renders
`tsx
import { FormEvent, ChangeEvent, useEffect, forwardRef, ForwardedRef } from "react";
import { useController } from "@figliolia/react-hooks";class FormCtrl {
public formData: Record = {};
public initialize() {
// some initialization logic
}
public destroy() {
// some tear down logic
}
public onChange = (e: ChangeEvent) => {
this.formData[e.target.name] = e.target.value;
}
public onSubmit = (e: FormEvent) => {
for(const key in this.formData) {
// validate form data
}
void fetch("/post-data", {
method: "POST",
body: JSON.stringify(this.formData)
}).then(() => {
this.formData = {};
}).catch(() => {})
}
}
export const MyForm = forwardRef(function(_: Propless, ref: ForwardedRef) {
const controller = useController(new FormCtrl());
// To expose your controller to other components:
useImperativeHandle(
ref,
() => controller,
[controller]
)
// controller is stable and always defined between renders
// so this useEffect only fires on mount/unmount
useEffect(() => {
controller.initialize();
return () => {
controller.destroy()
}
}, [controller])
// all public methods are exposed to use in component
// logic:
return (
);
});
`$3
Given a callback and a wait period, returns a debounced implementation of that callback. The scheduled callback will automatically cancel if the component unmounts.
`tsx
const AutoComplete = () => {
const input = useRef(null);
const [suggestions, setSuggestions] = useState([]); const fetchSuggestions = useCallback(() => {
fetch(
/api?search=${input.current.value})
.then(res => res.json())
.then(setSuggestions)
.catch(() => {});
}, []); const fetchMore = useDebouncer(fetchData, 200);
return (
id='search'
ref={input}
type="search"
placeholder="Search"
list='searchSuggestions'
onChange={fetchMore.execute} />
);
}
`$3
Given a callback and a wait period, returns a throttled implementation of that callback
`tsx
const ThreeDButton = () => {
const button = useRef(null);
const [rotationX, setRotationX] = useState(0);
const [rotationY, setRotationY] = useState(0);
const [shadow, setShadow] = useState( 0px 0px 0px rgba(0,0,0,0)); const onMouseMove = useCallback((e: MouseEvent) => {
const { top, left, width, height } = button.getBoundingClientRect();
const X = e.clientX - left;
const Y = e.clientY - top;
const midX = width / 2;
const midY = height / 2;
setRotationX((Y - midY) * 0.05);
setRotationY((X - midX) * -0.05);
setShadow(
${(X - midX) 0.15}px ${(Y - midY) 0.15}px ${Math.max(X, Y) / 5}px rgba(0,0,0,0.2));
}, []) const animator = useThrottler(onMouseMove, 100);
return (
ref={button}
onMouseMove={animator.execute}
style={{
boxShadow: shadow,
transform:
rotateX(${rotationX}deg) rotateY(${rotationY}deg),
}}
>
3D Button!
);
}
`$3
Returns the user's specified locale and rerenders whenever it changes
`tsx
export const useLocalizedNumber = (value: number) => {
const locale = useLocale();
return useMemo(() => {
return new Intl.NumberFormat(locale, {}).format(value);
}, [value, locale]);
}
`$3
A hook that will respond to keydown events if target element comes into focus
`tsx
export const MyAllyComponent = () => {
const node = useRef(null);
const onEnter = useCallback(() => {
// Take some action when this component is focused
// and the user presses the enter key
}, []);
const onDelete = useCallback(() => {
// Take some action when this component is focused
// and the user presses the delete key
}, []);
useFocusedKeyListener(onEnter, "Enter");
useFocusedKeyListener(onEnter, "Delete");
return (
{/ ...Markup /}
);
}
`$3
A hook that returns a ref and a dimensions object containing width and height values. When the provided ref is attached to a DOM element, the hook will rerender a new dimensions object each time the element's width or height change.
`tsx export const MyResizingComponent = ({ children }) => {
const [ref, dimensions] = useNodeDimensions();
useEffect(() => {
if(dimensions) {
console.log('This component resized!', dimensions);
}
}, [dimensions]);
return (
{children}
);
}
`$3
A hook that accepts any number of ref callbacks or ref objects and returns a single ref callback. Useful for when managing multiple refs that need to a attach to a single component
`tsx
export const MyComponent = ({ children, forwardedRef }) => {
const [ref, dimensions] = useNodeDimensions();
const mergedRefs = useMergedRefs(ref, forwardedRef);
return (
{children}
);
}
`Motivation
Since migrating to the hooks API at React v16, certain pieces of application functionality became more combersome or repetitive to implement. Such as:
1. Using and cleaning up timeouts/intervals to ensure no memory leaks
2. Ensuring a callback runs only on mount/unmount (while remaining up-to-date with most the recent props)
3. Adding micro-interactions to forms
4. Creating stable references to class-instances without needing to check if a ref's current value is null`The hooks found in the library are the ones I find myself reaching for day-to-day, regardless of the product I'm working on. I hope they save you some time too!