Small package of specialized hooks meant to solve interaction scenarios in modern React applications.
npm install @spa-tools/interaction-hooksThe @spa-tools/interaction-hooks package is a small library of specialized hooks aimed at solving interaction scenarios in modern-day React applications.
#### Feature highlights include:
- Time-saving functionality
- Production-tested
- TypeScript First
- Zero Dependencies
- Tree-shakable
#### Hooks in this package:
- useCallOnce
- useDetectKeyDown
- useInfiniteScroll
- useIsHovered
- useIsOverflowed
- useQueryState
#### It's highly advised to first checkout the @spa-tools documentation site for a complete list of features, usage scenarios, guidance, and reference.
npm install @spa-tools/interaction-hooks
Hook that ensures code runs once and only once no matter how many times your component re-renders.
``jsx
import { useEffect, useState } from 'react';
import { useCallOnce } from '@spa-tools/interaction-hooks';
// this is the function we want to call only once
function logOnce(message: string) {
console.log('This will only log once:', message);
}
export function UseCallOnceHookExample() {
const [time, setTime] = useState(new Date());
// here we setup some code to force a re-render every second
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(interval);
}, []);
// here we use the hook to call the function
// so that it's guaranteed to only execute once
useCallOnce(logOnce, 'Hello, world!');
return
Current time: {time.toLocaleTimeString()}
;$3
Hook that watches for requeste keys/modifiers to be pressed.
`jsx
import { useEffect, useRef } from 'react';
import { useDetectKeyDown } from '@spa-tools/interaction-hooks';function UseDetectKeyDownExample() {
// here we wire up a ref for the submit button that we will auto-click
const submitButtonRef = useRef(null);
// here we ask the hook to set onKeyDownInput1KeyDetected to true
// when the Shift-Ctrl-P keys are pressed
const [onKeyDownInput1, pShiftControlKeysDetected] = useDetectKeyDown('P', ['Shift', 'Control']);
// here we ask the hook to auto-click the submit button when
// the Enter key is pressed
const [onKeyDownInput2] = useDetectKeyDown('Enter', submitButtonRef);
useEffect(() => {
// we simply alert when the Shift-Ctrl-P keys are detected
if (pShiftControlKeysDetected) {
alert('Shift-Ctrl-P detected!');
}
}, [pShiftControlKeysDetected]);
return (
onClick={() => {
alert('Submit button clicked!');
}}
ref={submitButtonRef}
>
Submit
);
}
`$3
Hook that enables infinite scroll behavior.
`jsx
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallEndpoint } from '@spa-tools/api-client';
import { useInfiniteScroll } from '@spa-tools/interaction-hooks';// here we create a custom hook to fetch recipes from a server
// using the useCallEndpoint hook from the @spa-tools/api-client package
function useGetRecipes() {
return useCallEndpoint(
'https://dummyjson.com/recipes',
{
requestOptions: { recordLimit: 10 },
serverModelOptions: { jsonDataDotPath: 'recipes' },
},
// we pass true to enable appending of new records
true
);
}
function UseInfiniteScrollExample() {
// this will hold the ref to our scroll target, which is just a div that we
// place below our list of recipes to act as a sentinel for scroll intersection
const scrollTargetRef = useRef(null);
const [total, setTotal] = useState(0);
const [count, setCount] = useState(0);
const [getRecipes, recipesResult, isRecipesCallPending] = useGetRecipes();
// anytime our scroll target is intersected for vertical scroll, the hook
// will return true, which is how we know to fetch the next page of recipes
const isScrolling = useInfiniteScroll(scrollTargetRef);
const handleGetRecipes = useCallback(() => {
const recordCount = recipesResult?.data?.length ?? -1;
const totalCount = recipesResult?.total ?? 0;
setCount(recordCount);
setTotal(totalCount);
if (!isRecipesCallPending && recordCount < totalCount) {
getRecipes();
}
}, [getRecipes, isRecipesCallPending, recipesResult?.data?.length, recipesResult?.total]);
useEffect(() => {
if (isScrolling) {
// if the infinite scroll says we're scrolling,
// then we retrieve the next page of recipes
handleGetRecipes();
}
}, [handleGetRecipes, isScrolling]);
return (
{count && total ? ${count === total ? All ${count} : ${count} of ${total}} recipes retrieved! : ''}
{count && total && count < total ? ' (scroll recipe list to load more)' : ''}
{recipesResult?.data?.map((recipe) => - {recipe.name}
)}
{isRecipesCallPending && Loading recipes...}
);
}
`$3
Hook that watches elements for hover state.
`jsx
import { useRef } from 'react';
import { useIsHovered } from '@spa-tools/interaction-hooks';function UseIsHoveredExample() {
// here we simply setup refs to all elements we want
// to track hover state for
const buttonRef = useRef(null);
const inputRef = useRef(null);
const spanRef = useRef(null);
// yes! you can also track hover state for an array of elements
const arrayRef = useRef([]);
// then we use different hook instances to track the
// hover state for the above element refs
const isButtonHovered = useIsHovered(buttonRef);
const isInputHovered = useIsHovered(inputRef);
const isSpanHovered = useIsHovered(spanRef);
const isButtonArrayHovered = useIsHovered(arrayRef);
const getHoverStateText = () => {
if (isButtonHovered) {
return 'Very first button is hovered!';
}
if (isInputHovered) {
return 'Input is hovered!';
}
if (isSpanHovered) {
return 'Text is hovered!';
}
if (isButtonArrayHovered) {
return 'One of the six buttons from cluster is hovered!';
}
return 'Nothing is hovered!';
};
return (
Don't listen to them, hover over{' '}
this text
{' '}
instead!
{getHoverStateText()}
);
}
`$3
Hook that watches for when an element's content is overflowed.
`jsx
import { useRef } from 'react';
import { useIsOverflowed } from '@spa-tools/interaction-hooks';function UseIsOverflowedExample() {
const sectionRef = useRef(null);
const isVerticallyOverflowed = useIsOverflowed(sectionRef);
return (
Scroll me
{isVerticallyOverflowed ? 'Overflowed vertically' : 'Not overflowed vertically'}
);
}
`$3
Hook that manages interaction with the URLs querystring.
`jsx
import { useQueryState } from '@spa-tools/interaction-hooks';// here we edefine the shape for our query state
interface SortColumnInfo {
sortColumn: string;
sortDirection: 'ASC' | 'DESC';
}
function UseQueryStateExample() {
// we pass true to the useQueryState hook to enable the cache (i.e. localStorage)
// feature so that the query state for this view is remembered across page reloads
const { queryState, setQueryState } = useQueryState(true);
return (
onClick={() => {
// here we set the query state to sort by age in descending order
setQueryState({ sortColumn: 'age', sortDirection: 'DESC' });
}}
>
Sort by Age (DESC)
onClick={() => {
// here we set the query state to sort by name in ascending order
setQueryState({ sortColumn: 'name', sortDirection: 'ASC' });
}}
>
Sort by Name (ASC)
onClick={() => {
// here we hard reload the page with querystring removed
// to test the cache feature
window.location.href = window.location.href.split('?')[0];
}}
>
Hard reload to test cache
onClick={() => {
// here we clear the query state
setQueryState(null);
}}
>
Clear sort settings
{queryState === null ? (
Click one of the "Sort by" buttons and watch the browser's URL and also how this text changes!
) : (
Sort column {queryState.sortColumn} in {queryState.sortDirection} direction!
)}
);
}
``View the @spa-tools documentation site for complete reference.
If you're interested in contributing to @spa-tools, please first create an issue on the @spa-tools monorepo in GitHub
or comment on an already open issue. From there we can discuss the feature or bugfix you're interested in and how best to approach it.
All packages in @spa-tools require 100% unit test coverage. This is a condition for all PRs to be merged whether you're adding a new feature or fixing a bug.
All packages in @spa-tools are licensed under the MIT license. Copyright © 2024, Ryan Howard (rollercodester). All rights reserved.