Though this article may be enjoyed by anyone curious, the content is best suited to TypeScript developers who have some experience with React.
HOOKS
Starting off simple,
we want our domains visible to the user as long as possible, and we'd rather not eat their bread crumbs. useNewTab.
useNewTab
const useNewTab = (url: string) =>
window.open(url, "_blank")
Often all we really want is to open a tab, but the
.open
method call actually returns a handle to the new window, allowing for some neat possibilities programmatically. At the behest of cross-site protections, of course.
I recently encountered a situation where
the client ordered a full-size pdf document display on viewports of laptop size or larger, but wanted a simple download link for the pdf on mobile viewports. I needed to conditionally render components based on viewport size. Enter useViewportQuery.
useViewportQuery
function useViewportQuery() {
const [dimensions, setDimensions] = useState([window.innerWidth, window.innerHeight])
useEffect(() => {
function handleResize(_: UIEvent) {
// There are other dimension properties on the window as well,
// such as outerWidth and outerHeight
setDimensions([window.innerWidth, window.innerHeight])
}
window.addEventListener('resize', handleResize)
// Don't forget to clean up!
return () => window.removeEventListener('resize', handleResize)
}, [])
return dimensions;
}
// Since the hook returns a tuple, we can call it like this:
const [width, height] = useViewportQuery();
Dig this one? Check out the Resize Observer API for related strategies.
The browser is full of delicious APIs
which a couple of our custom hooks make use of. This first one, using the Clipboard API, can make copying important text content from the webpage breezy and painless for our users.
Bear in mind that there are many different ways to go about this, so I've included a couple different examples. Be sure to research browser compatibility before making a decision.
useClipboard
/*
Here our goal is to simply write text to the clipboard,
so we return a boolean so we can handle any errors at the call site.
*/
const useClipboardWriter = async (text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text)
return true;
} catch (err) {
console.error("Failed to write to clipboard ... ")
return false;
}
}
/*
Here's a more complete implementation, with most of the code
simply accounting for the fact that the clipboard API is
asynchronous and error-prone.
*/
export function useClipboardManager() {
const { clipboard } = navigator;
const read = async () => {
try {
await clipboard.readText();
} catch (err) {
throw new Error("Failed to read clipboard text...")
}
};
const add = async (text: string) => {
try {
// read-append-overwrite
// local storage vibes?
const current = await read();
await clipboard.writeText(current + text);
} catch {
throw new Error("Failed to add text to clipboard...")
}
}
const clear = async () => {
try {
await clipboard.writeText("")
} catch {
throw new Error("Failed to clear clipboard text...")
}
}
return [read, add, clear]
}
/* Similar to how react hooks such as useState return setters,
we return functions that manage the clipboard's state.
We don't need to store anything in our component's state
because the browser stores the clipboard for us.
*/
const [read, add, clear] = useClipboardManager();
Note that the user must provide permissions for your web app to access the clipboard API.
When I first discovered the Intersection Observer,
I couldn't help wondering how I hadn't encountered it sooner. The API offered the ideal solution to problems I had previously encountered, such as triggering an animation when an element comes into view.
useIntersectionObserver
function useIntersectionObserver(ref: MutableRefObject<any>) {
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
// Define what happens when our referenced element is 'observed',
// or has been 'intersected'
const options = {
// root: window, // We can set the visibility basis manually here,
// but the default of 'window' is what we want
threshold: 1, // 100% of the element must be visible within the 'root'
}
const observer = new IntersectionObserver(
([entry]) => {
// Synchronize state locally
setIntersecting(entry.isIntersecting);
},
options,
);
// Observe our element if a reference exists
ref.current && observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref]);
return isIntersecting;
}
// Now that our hook is established, all we have to do
// is call pass our element reference within the component
// and respond to the boolean state accordingly,
const intersectRef = useRef(null);
const isIntersecting = useIntersectionObserver(intersectRef);
// e.g. if (isIntersecting) { playAnimation() };
I believe that user interaction is the key feature
that sets software apart from other forms of media. Unlike audio, video, or text, software depends on the user's participation, and it is up to the engineer to ensure the user feels that their interactions are impactful or meaningful in some way.
The mouse provides a rich source of eventfulness for the UX designer. Every movement, click, and scroll can be tracked and responded to accordingly.
The following hook, useCursorPosition, makes it incredibly simple to track the state of the mouse cursor.
useCursorPosition
type CursorPosition = { x: number; y: number };
function useCursorPosition(): CursorPosition {
const [position, setPosition] = useState<CursorPosition>({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
return position;
}
// Not much to comment on here.
// The hook returns a simple object with {x, y} mouse coordinates.
const { x, y } = useCursorPosition();
Conclusion
The possibilities with custom hooks are endless, and I fully expect to amend this list in the near future as my adventure continues.
In the meantime:
Keep going, keep growing, and happy coding!
Top comments (0)