npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@figliolia/react-hooks

v1.1.0

Published

A small collection of simple React Hooks you're probably rewriting on a regular basis

Downloads

158

Readme

React Hooks

A small collection of simple React Hooks you're probably rewriting on a regular basis

  1. 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
  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

Installation

npm i @figliolia/react-hooks
# or
yarn add @figliolia/react-hooks

Basic Usage

useMount

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
  );
}

useUnmount

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
  );
}

useLoadingState

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>
  );
}

useFormState

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>
  );
}

useTimeout

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>
  );
}

useAnimationFrame

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}%` }}>
    </>
  );
}

useController

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>
  );
});

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.

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>
  );
}

useThrottler

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>
  );
}

useLocale

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]);
}

useFocusedKeyListener

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>
  );  
}

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
  5. 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!