A small collection of simple React Hooks you're probably rewriting on a regular basis
- useMount - Execute a callback on first mount
- useUnmount - Execute a callback on unmount
- useLoadingState - Provides a controller for getting/setting loading, error, and success states for a UI interaction
- useFormState - Provides a controller with access to form data, loading, error, and success states for a form submission
- useTimeout - Execute deferred callbacks that'll automatically clean themselves up when the hook unmounts
-
useAnimationFrame - Execute callbacks in
requestAnimationFrame
that'll automatically stop/cancel if the hook unmounts - useController - Maintains a stable reference to an object instance (created by a component) between renders
- 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.
- useThrottler - Given a callback and a wait period, returns a throttled implementation of that callback
- useLocale - Returns the user's specified locale and rerenders whenever it changes
- useFocusedKeyListener - A hook that will respond to keydown events if target element comes into focus
- 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
-
useNodeDimensions - A hook that returns a
ref
and adimensions
object containingwidth
andheight
values. When the providedref
is attached to aDOM
element, the hook will rerender a newdimensions
object each time the element'swidth
orheight
change.
npm i @figliolia/react-hooks
# or
yarn add @figliolia/react-hooks
Execute a callback on the first mount of a hook or component
import { useMount } from "@figliolia/react-hooks";
export const MyComponent = () => {
useMount(() => {
// Executes on first mount!
});
return (
// JSX
);
}
Execute a callback when a hook or component unmounts - note - this callback will not run when props change
import { useUnmount } from "@figliolia/react-hooks";
export const MyComponent = () => {
useUnmount(() => {
// Executes on unmount!
});
return (
// JSX
);
}
Provides a controller for getting/setting loading, error, and success states for a UI interaction
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 (
<button
className={classes}
onClick={onClick}>Click Me!</button>
);
}
Provides a controller with access to form data, loading, error, and success states for a form submission
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 (
<form onSubmit={onSubmit}>
<input type="text" name="name" />
<input type="email" name="email" />
<input type="password" name="password" />
<button className={classes}>Submit!</button>
</form>
);
}
Execute deferred callbacks that'll automatically clean themselves up when the hook unmounts
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 (
<button onClick={onClick}>{count}</button>
);
}
Execute callbacks in requestAnimationFrame
that'll automatically stop/cancel if the hook unmounts
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 (
<>
<button onClick={onClick}>Progress: {progress}%</button>
<div
className="progress-bar"
style={{ width: `${progress}%` }}>
</>
);
}
Maintains a stable reference to an object instance (created by a component) between renders
import { FormEvent, ChangeEvent, useEffect, forwardRef, ForwardedRef } from "react";
import { useController } from "@figliolia/react-hooks";
class FormCtrl {
public formData: Record<string, string> = {};
public initialize() {
// some initialization logic
}
public destroy() {
// some tear down logic
}
public onChange = (e: ChangeEvent<HTMLInputElement>) => {
this.formData[e.target.name] = e.target.value;
}
public onSubmit = (e: FormEvent<HTMLFormElement>) => {
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<FormCtrl>) {
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 (
<form>
<input
type="text"
name="name"
onChange={controller.onChange} />
<input
type="email"
name="email"
onChange={controller.onChange} />
<input
type="password"
name="password"
onChange={controller.onChange} />
<button
type="button"
onClick={controller.onSubmit}>Submit!</button>
</form>
);
});
Given a callback and a wait period, returns a debounced implementation of that callback. The scheduled callback will automatically cancel if the component unmounts.
const AutoComplete = () => {
const input = useRef<HTMLInputElement>(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 (
<div>
<input
id='search'
ref={input}
type="search"
placeholder="Search"
onChange={fetchMore}
list='searchSuggestions' />
<datalist list='searchSuggestions'>
{
suggestions.map(suggestion => {
return (
<option key={suggestion} value={suggestion} />
);
})
}
</datalist>
</div>
);
}
Given a callback and a wait period, returns a throttled implementation of that callback
const ThreeDButton = () => {
const button = useRef<HTMLButtonElement>(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<HTMLButtonElement>) => {
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 animate = useThrottler(onMouseMove, 100);
return (
<button ref={button} onMouseMove={animate}>
3D Button!
</button>
);
}
Returns the user's specified locale and rerenders whenever it changes
export const useLocalizedNumber = (value: number) => {
const locale = useLocale();
return useMemo(() => {
return new Intl.NumberFormat(locale, {}).format(value);
}, [value, locale]);
}
A hook that will respond to keydown events if target element comes into focus
export const MyAllyComponent = () => {
const node = useRef<HTMLDivElement>(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 (
<div ref={node} role="button">
{/* ...Markup */}
</div>
);
}
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.
export const MyResizingComponent = ({ children }) => {
const [ref, dimensions] = useNodeDimensions();
useEffect(() => {
if(dimensions) {
console.log('This component resized!', dimensions);
}
}, [dimensions]);
return (
<div ref={ref}>{children}</div>
);
}
Since migrating to the hooks API at React v16, certain pieces of application functionality became more combersome or repetitive to implement. Such as:
- Using and cleaning up timeouts/intervals to ensure no memory leaks
- Ensuring a callback runs only on mount/unmount (while remaining up-to-date with most the recent props)
- Adding micro-interactions to forms
- Creating stable references to class-instances without needing to check if a
ref's
current value isnull
- Ensuring component-logic can be extracted into controllers as well as hooks
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!