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

use-stepper

v4.0.2

Published

React hook to manage a numeric stepper/spinbutton

Downloads

2,339

Readme

use-stepper

React hook to manage a numeric stepper/spinbutton

npm Version Build Status Code Coverage

The problem

A numeric stepper or "spinbutton" (decrement button, input, increment button) is deceptively non-trivial to implement. You have to manage the minimum, the maximum, the input itself displaying the current value (conceptually numeric, but ultimately a string in HTML), allowing a user to type freely and hopefully arrive at a valid number (e.g. "-" is NaN but you have to let them type it so they can get to "-4", so simply parsing their input as a number is insufficient), interpreting the input's value on blur, etc.

This solution

This React hook manages all this for you so you only have to worry about the styling. It returns the current value, functions for manual value manipulation, and a collection of prop getters to spread across your form elements to make it all work together. It also includes the ability to provide your own custom state reducer to enable ultimate control over what actions like "increment", "decrement", "coerce", etc. mean in your application.

Table of Contents

Installation

Using npm:

npm install use-stepper # or, with yarn: yarn add use-stepper

This package also depends on react 16.8.0 or newer. Please make sure you have that installed as well.

Usage

import { useStepper } from 'use-stepper';

function MyStepper() {
  const { getFormProps, getInputProps, getIncrementProps, getDecrementProps } =
    useStepper();

  return (
    <form {...getFormProps()}>
      <button {...getDecrementProps()}>-</button>
      <input {...getInputProps()} />
      <button {...getIncrementProps()}>+</button>
    </form>
  );
}

Basic Options

The hook accepts an options object as its only parameter. The entries for the basic options are as follows:

defaultValue

number | optional, default: 0

The initial value and the value to use as a fallback upon invalid manual input.

step

number | optional, default: 1

The amount by which to increment or decrement the current value.

min

number | optional, default: -Number.MAX_VALUE

The minimum value allowed.

max

number | optional, default: Number.MAX_VALUE

The maximum value allowed.

Advanced Options

enableReinitialize

boolean | optional, default: false

Controls whether the current value (if unchanged) will update to the new default if defaultValue changes.

stateReducer

function(state: object, action: object): object | optional

Changes to the state of the hook are applied using React's built-in useReducer hook. This function replaces the default reducer implementation, which allows you to author your own logic to execute when an action is dispatched. This gives you ultimate control in the event you wish to constrain the value or otherwise modify the default behavior. It gives you the current state and the action to execute, and you return the new state.

  • state: the current state of the hook
  • action: the action to execute

State is just an object with a value key (note: value is a string, so it should be converted to a number before performing mathematical operations on it).

Actions have a type field and an optional payload field. The possible action types are discoverable through useStepper.actionTypes (e.g. useStepper.actionTypes.increment). The default reducer is also available at useStepper.defaultReducer in case you want to defer to the default implementation in some cases (e.g. for the default/unhandled action, perhaps).

Example

Constrain the stepper to only use whole numbers:

function IntegerStepper({ min, max, defaultValue }) {
  function validValueClosestTo(desiredNumericValue) {
    return String(Math.min(max, Math.max(desiredNumericValue, min)));
  }

  function integerReducer(state, action) {
    const integerValue = parseInt(state.value, 10);
    switch (action.type) {
      case useStepper.actionTypes.increment: {
        return { value: validValueClosestTo(integerValue + 1) };
      }
      case useStepper.actionTypes.decrement: {
        return { value: validValueClosestTo(integerValue - 1) };
      }
      case useStepper.actionTypes.coerce: {
        if (Number.isNaN(integerValue)) {
          return { value: String(defaultValue) };
        }
        const newValue = validValueClosestTo(integerValue);
        if (newValue !== state.value) {
          return { value: newValue };
        }
        return state;
      }
      default:
        return useStepper.defaultReducer(state, action);
    }
  }

  const { getInputProps, getIncrementProps, getDecrementProps } = useStepper({
    min,
    max,
    defaultValue,
    stateReducer: integerReducer,
  });

  return (
    <>
      <button {...getDecrementProps()}>-</button>
      <input {...getInputProps()} />
      <button {...getIncrementProps()}>+</button>
    </>
  );
}

Return Value

The hook returns an object with the following shape:

| key | type | category | | ------------------- | -------- | ---------------------------- | | getFormProps | function | prop getter | | getInputProps | function | prop getter | | getIncrementProps | function | prop getter | | getDecrementProps | function | prop getter | | value | string | state | | increment | function | setter | | decrement | function | setter | | setValue | function | setter |

prop getters

These functions are used to apply props to the elements that you render. This gives you maximum flexibility to render what, when, and wherever you like. You call these on the element in question (for example: <input {...getInputProps()})). It's advisable to pass all your props to that function rather than applying them on the element yourself to avoid your props being overridden (or overriding the props returned). For example: getInputProps({ onChange: myChangeHandler }).

| key | type | description | | ------------------- | ------------ | ---------------------------------------------------------------------------- | | getFormProps | function({}) | returns the props you should apply to the form element for submit handling | | getInputProps | function({}) | returns the props you should apply to the input element (includes value) | | getIncrementProps | function({}) | returns the props you should apply to the increment button element | | getDecrementProps | function({}) | returns the props you should apply to the decrement button element |

state

These are the values that represent the internal state of the hook.

| key | type | description | | ------- | ------ | -------------------------------- | | value | string | the current value of the stepper |

setters

These functions are exposed to provide manual manipulation of the internal value.

| key | type | description | | ----------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | increment | function() | increments the value | | decrement | function() | decrements the value | | setValue | function(str) | sets the value (note: the argument passed will be coerced to a valid value within the specified range or fall back to the default value if not a valid number) |

Other Solutions

  • @zag-js/number-input is a more full-featured alternative driven by state machines and maintained by the team behind the popular Chakra UI project.
  • react-stepper-primitive by Andrew Joslin was the prime source of inspiration for this hook.

If you know of any others, please send a pull request to add them here.

License

MIT