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 🙏

© 2025 – Pkg Stats / Ryan Hefner

react-impulse

v3.1.2

Published

The clean and natural React state management

Downloads

246

Readme

react-impulse

codecov known vulnerabilities types npm version

The clean and natural React state management.

# with yarn
yarn add react-impulse

# with npm
npm install react-impulse

Quick start

Impulse is a box holding any value you want, even another Impulse! All components that execute the Impulse#getValue during the rendering phase enqueue re-render whenever the Impulse value updates.

import { Impulse, useScope } from "react-impulse"

const Input: React.FC<{
  type: "email" | "password"
  value: Impulse<string>
}> = ({ type, value }) => {
  const scope = useScope()

  return (
    <input
      type={type}
      value={value.getValue(scope)}
      onChange={(event) => value.setValue(event.target.value)}
    />
  )
}

const Checkbox: React.FC<{
  checked: Impulse<boolean>
  children: React.ReactNode
}> = ({ checked, children }) => {
  const scope = useScope()
  // the `scope` is passed to the `Impulse#getValue` method
  return (
    <label>
      <input
        type="checkbox"
        checked={checked.getValue(scope)}
        onChange={(event) => checked.setValue(event.target.checked)}
      />

      {children}
    </label>
  )
}

Once created, Impulses can travel thru your components, where you can set and get their values:

import { Impulse, useScope } from "react-impulse"

const SignUp: React.FC = () => {
  const scope = useScope()
  const [{ username, password, isAgreeWithTerms }] = React.useState({
    username: Impulse(""),
    password: Impulse(""),
    isAgreeWithTerms: Impulse(false),
  })

  return (
    <form>
      <Input type="email" value={username} />
      <Input type="password" value={password} />
      <Checkbox checked={isAgreeWithTerms}>I agree with terms of use</Checkbox>

      <button
        type="button"
        disabled={!isAgreeWithTerms.getValue(scope)}
        onClick={() => {
          tap((scope) => {
            api.submitSignUpRequest({
              username: username.getValue(scope),
              password: password.getValue(scope),
            })
          })
        }}
      >
        Sign Up
      </button>
    </form>
  )
}

API

A core piece of the library is the Impulse class - a box that holds value. The value might be anything you like as long as it does not mutate. The class instances are mutable by design, but other Impulses can use them as values.

Impulse

The impulse type and factory. Returns an impulse instance which extends ReadableImpulse and WritableImpulse interfaces.

Impulse<T>(): Impulse<undefined | T>

Impulse<T>(
  initialValue: T,
  options?: ImpulseOptions<T>
): Impulse<T>
  • [initialValue] is an optional initial value. If not defined, the Impulse's value is undefined but it still can specify the value's type.
  • [options] is an optional ImpulseOptions object.
    • [options.compare] when not defined or null then Object.is applies as a fallback.
const Counter: React.FC = () => {
  const [count, setCount] = React.useState(0)

  const [countImpulse] = React.useState(() => Impulse(count))

  React.useEffect(() => {
    // sync the state with the Impulse
    countImpulse.setValue(count)
  }, [count, countImpulse])

  useScopedEffect(
    (scope) => {
      // sync the Impulse with the state
      setValue(countImpulse.getValue(scope))
    },
    [countImpulse],
  )

  return (
    <button type="button" onClick={() => countImpulse.setValue((x) => x + 1)}>
      {count}
    </button>
  )
}
import { useSelector, useDispatch } from "react-redux"

const Counter: React.FC = () => {
  const count = useSelector((state) => state.count)
  const dispatch = useDispatch()

  const [countImpulse] = React.useState(() => Impulse(count))

  React.useEffect(() => {
    // sync the state with the Impulse
    countImpulse.setValue(count)
  }, [count, countImpulse])

  useScopedEffect(
    (scope) => {
      // sync the Impulse with the state
      dispatch({ type: "SET_COUNT", payload: countImpulse.getValue(scope) })
    },
    [countImpulse, dispatch],
  )

  return (
    <button type="button" onClick={() => countImpulse.setValue((x) => x + 1)}>
      {count}
    </button>
  )
}

Impulse derived

Impulse<T>(
  getter: ReadableImpulse<T> | ((scope: Scope) => T),
  options?: ImpulseOptions<T>,
): ReadonlyImpulse<T>

Impulse<T>(
  getter: ReadableImpulse<T> | ((scope: Scope) => T),
  setter: WritableImpulse<T> | ((value: T, scope: Scope) => void),
  options?: ImpulseOptions<T>,
): Impulse<T>
  • getter is either anything that implements the ReadableImpulse interface or a function to read the derived value from the source.
  • [setter] is either anything that implements the WritableImpulse interface or a function to write the derived value back to the source. When not defined, the resulting Impulse is readonly.
  • [options] is an optional ImpulseOptions object.
    • [options.compare] when not defined or null then Object.is applies as a fallback.

A function that creates a new derived Impulse. A derived Impulse is an Impulse that keeps the derived value in memory and updates it whenever the source value changes. A source is another Impulse or multiple Impulses.

const Drawer: React.FC<{
  isOpen: Impulse<boolean>
  children: React.ReactNode
}> = ({ isOpen, children }) => {
  const scope = useScope()

  if (!isOpen.getValue(scope)) {
    return null
  }

  return (
    <div className="drawer">
      {children}

      <button type="button" onClick={() => isOpen.setValue(false)}>
        Close
      </button>
    </div>
  )
}

const ProductDetailsDrawer: React.FC<{
  product: Impulse<undefined | Product>
}> = ({ product }) => {
  const isOpen = React.useMemo(() => {
    return Impulse(
      (scope) => product.getValue(scope) != null,
      (open) => {
        if (!open) {
          product.setValue(undefined)
        }
      },
    )
  }, [product])

  return (
    <Drawer isOpen={isOpen}>
      <ProductDetails product={product} />
    </Drawer>
  )
}
const Checkbox: React.FC<{
  checked: Impulse<boolean>
}> = ({ checked, children }) => {
  const scope = useScope()

  return (
    <input
      type="checkbox"
      checked={checked.getValue(scope)}
      onChange={(event) => checked.setValue(event.target.checked)}
    />
  )
}

const Agreements: React.FC<{
  isAgreeWithTermsOfUse: Impulse<boolean>
  isAgreeWithPrivacy: Impulse<boolean>
}> = ({ isAgreeWithTermsOfUse, isAgreeWithPrivacy }) => {
  const isAgreeWithAll = React.useMemo(() => {
    return Impulse(
      (scope) =>
        isAgreeWithTermsOfUse.getValue(scope) &&
        isAgreeWithPrivacy.getValue(scope),
      (agree) => {
        isAgreeWithTermsOfUse.setValue(agree)
        isAgreeWithPrivacy.setValue(agree)
      },
    )
  }, [isAgreeWithTermsOfUse, isAgreeWithPrivacy])

  return (
    <div>
      <Checkbox checked={isAgreeWithTermsOfUse}>
        I agree with terms of use
      </Checkbox>
      <Checkbox checked={isAgreeWithPrivacy}>
        I agree with privacy policy
      </Checkbox>

      <hr />

      <Checkbox checked={isAgreeWithAll}>I agree with all</Checkbox>
    </div>
  )
}

Impulse#getValue

Impulse<T>#getValue(scope: Scope): T

An Impulse instance's method that returns the current value.

  • scope is Scope that tracks the Impulse value changes.
  • [select] is an optional function that applies to the current value before returning.
const count = Impulse(3)

tap((scope) => {
  count.getValue(scope) // === 3
})

Impulse#setValue

Impulse<T>#setValue(
  valueOrTransform: T | ((currentValue: T, scope: Scope) => T),
): void

An Impulse instance's method to update the value.

  • valueOrTransform is the new value or a function that transforms the current value.
tap((scope) => {
  const isActive = Impulse(false)

  isActive.setValue((x) => !x)
  isActive.getValue(scope) // true

  isActive.setValue(false)
  isActive.getValue(scope) // false
})

💡 If valueOrTransform argument is a function it acts as batch.

💬 The method returns void to emphasize that Impulse instances are mutable.

Impulse#clone

Impulse<T>#clone(
  options?: ImpulseOptions<T>,
): Impulse<T>

Impulse<T>#clone(
  transform?: (value: T, scope: Scope) => T,
  options?: ImpulseOptions<T>,
): Impulse<T>

An Impulse instance's method for cloning an Impulse. When cloning a derived Impulse, the new Impulse is not deriving, meaning that it does not read nor write the value from/to the external source but instead it holds the derived value on the moment of cloning.

  • [transform] is an optional function that applies to the current value before cloning. It might be handy when cloning mutable values.
  • [options] is optional ImpulseOptions object.
    • [options.compare] when not defined it uses the compare function from the origin Impulse, when null the Object.is function applies to compare the values.
const immutable = Impulse({
  count: 0,
})
const cloneOfImmutable = immutable.clone()

const mutable = Impulse({
  username: Impulse(""),
  blacklist: new Set(),
})
const cloneOfMutable = mutable.clone((current) => ({
  username: current.username.clone(),
  blacklist: new Set(current.blacklist),
}))

Scope

Scope is a bridge that connects Impulses with host components. It tracks the Impulses' value changes and enqueues re-renders of the host components that read the Impulses' values. The only way to read an Impulse's value is to call the Impulse#getValue method with Scope passed as the first argument. The following are the primary ways to create a Scope:

  • useScope hook returns a Scope instance. It is a handy way to create a single component/hook-wide scope. It lacks granularity but is easy to use.
  • useScoped hook provides the scope argument. It can be used in custom hooks or inside components to narrow down the re-rendering scope.
  • subscribe function provides the scope argument. It is useful outside of the React world.
  • batch function provides the scope argument. Use it to optimize multiple Impulses updates or to access the Impulses' values inside async operations.
  • untrack function provides the scope argument. Use it when you need to read Impulses' values without reactivity.
  • useScopedCallback, useScopedMemo, useScopedEffect, useScopedLayoutEffect hooks provide the scope argument. They are enchanted versions of the React hooks that provide the scope argument as the first argument.

useScoped

function useScoped<TValue>(impulse: ReadableImpulse<TValue>): TValue

function useScoped<T>(
  factory: (scope: Scope) => T,
  dependencies?: DependencyList,
  options?: UseScopedOptions<T>
): T
  • impulse is anything that implements the ReadableImpulse interface.
  • factory is a function that provides Scope as the first argument and subscribes to all Impulses calling the Impulse#getValue method inside the function.
  • dependencies is an optional array of dependencies of the factory function. If not defined, the factory function is called on every render.
  • [options] is an optional UseScopedOptions object.

The useScoped hook is the most common way to read Impulses' values. It either executes the factory function whenever any of the scoped Impulses' value update or reads the impulse value but enqueues a re-render only when the resulting value is different from the previous.

const useSumAllAndMultiply = ({
  multiplier,
  counts,
}: {
  multiplier: Impulse<number>
  counts: Impulse<Array<Impulse<number>>>
}): number => {
  return useScoped((scope) => {
    const sumAll = counts
      .getValue(scope)
      .map((count) => count.getValue(scope))
      .reduce((acc, x) => acc + x, 0)

    return multiplier.getValue(scope) * sumAll
  })
}

Components can scope watched Impulses to reduce re-rendering:

const Challenge: React.FC = () => {
  const [count] = React.useState(Impulse(0))
  // the component re-renders only once when the `count` is greater than 5
  const isMoreThanFive = useScoped((scope) => count.getValue(scope) > 5)

  return (
    <div>
      <Counter count={count} />

      {isMoreThanFive && <p>You did it 🥳</p>}
    </div>
  )
}

💬 The factory function is only for reading the Impulses' values. It should never call Impulse, Impulse#clone, or Impulse#setValue methods inside.

💡 Keep in mind that the factory function acts as a "reader" so you'd like to avoid heavy computations inside it. Sometimes it might be a good idea to pass a factory result to a separated memoization hook. The same is true for the compare function - you should choose wisely between avoiding extra re-renders and heavy comparisons.

💡 There is no need to memoize options.compare function. The hook does it internally.

useScope

Alias for useScoped(identity).

useScopedMemo

function useScopedMemo<T>(
  factory: (scope: Scope) => T,
  dependencies: DependencyList,
): T
  • factory is a function that provides Scope as the first argument and calculates a value T whenever any of the dependencies' values change.
  • dependencies is an array of values used in the factory function.

The hook is an enchanted React.useMemo hook.

useScopedCallback

function useScopedCallback<TArgs extends ReadonlyArray<unknown>, TResult>(
  callback: (scope: Scope, ...args: TArgs) => TResult,
  dependencies: DependencyList,
): (...args: TArgs) => TResult
  • callback is a function to memoize, the memoized function injects Scope as the first argument and updates whenever any of the dependencies values change.
  • dependencies is an array of values used in the callback function.

The hook is an enchanted React.useCallback hook.

useScopedEffect

function useScopedEffect(
  effect: (scope: Scope) => void | VoidFunction,
  dependencies?: DependencyList,
): void
  • effect is a function that provides Scope as the first argument and runs whenever any of the dependencies' values change. Can return a cleanup function to cancel running side effects.
  • [dependencies] is an optional array of values used in the effect function.

The hook is an enchanted React.useEffect hook.

useScopedLayoutEffect

The hook is an enchanted React.useLayoutEffect hook. Acts similar way as useScopedEffect.

~~useScopedInsertionEffect~~

There is no enchanted version of the React.useInsertionEffect hook due to backward compatibility with React from v16.12.0. The workaround is to use the native React.useInsertionEffect hook with the values extracted beforehand:

const usePrintSum = (left: number, right: Impulse<number>): void => {
  const rightValue = useScoped((scope) => right.getValue(scope))

  React.useInsertionEffect(() => {
    console.log("sum is %d", left + rightValue)
  }, [left, rightValue])
}

batch

function batch(execute: (scope: Scope) => void): void

The batch function is a helper to optimize multiple Impulses updates. It provides a Scope to the execute function so it is useful when an async operation accesses the Impulses' values.

const SumOfTwo: React.FC<{
  left: Impulse<number>
  right: Impulse<number>
}> = ({ left, right }) => {
  const scope = useScope()

  return (
    <div>
      <span>Sum is: {left.getValue(scope) + right.getValue(scope)}</span>

      <button
        onClick={() => {
          batch((scope) => {
            console.log(
              "resetting the sum %d",
              left.getValue(scope) + right.getValue(scope),
            )

            // enqueues 1 re-render instead of 2 🎉
            left.setValue(0)
            right.setValue(0)
          })
        }}
      >
        Reset
      </button>
    </div>
  )
}

tap

Alias for batch.

untrack

function untrack<TResult>(factory: (scope: Scope) => TResult): TResult
function untrack<TValue>(impulse: ReadableImpulse<TValue>): TValue

The untrack function is a helper to read Impulses' values without reactivity. It provides a Scope to the factory function and returns the result of the function. Acts as batch.

subscribe

function subscribe(listener: (scope: Scope) => void | VoidFunction): VoidFunction
  • listener is a function that provides Scope as the first argument and subscribes to changes of all Impulse instances that call the Impulse#getValue method inside the listener. If listener returns a function then it will be called before the next listener call.

Returns a cleanup function that unsubscribes the listener. The listener calls first time synchronously when subscribe is called.

It is useful for subscribing to changes of multiple Impulses at once:

const impulse_1 = new Impulse(1)
const impulse_2 = new Impulse(2)
const impulse_3 = new Impulse("calculating...")

const unsubscribe = subscribe((scope) => {
  if (impulse_1.getValue(scope) > 1) {
    const sum = impulse_2.getValue(scope) + impulse_3.getValue(scope)
    impulse_3.setValue(`done: ${sum}`)
  }
})

In the example above the listener will not react on the impulse_2 updates until the impulse_1 value is greater than 1. The impulse_3 updates will never trigger the listener, because the impulse_3.getValue(scope) is not called inside the listener.

💬 The subscribe function is the only function that injects Scope to the Impulse#toJSON() and Impulse#toString() methods because the methods do not have access to the scope:

const counter = Impulse({ count: 0 })

subscribe(() => {
  console.log(JSON.stringify(counter))
})
// console: {"count":0}

counter.setValue(2)
// console: {"count":2}

isImpulse

isImpulse<T, Unknown = unknown>(
  input: Unknown | Impulse<T>,
): input is Impulse<T>

isImpulse<T, Unknown = unknown>(
  scope: Scope,
  check: (value: unknown) => value is T,
  input: Unknown | Impulse<T>,
): input is Impulse<T>

A function that checks whether the input is an Impulse instance. If the check function is provided, it checks the Impulse's value to match the check function.

isDerivedImpulse

isDerivedImpulse<T, Unknown = unknown>(
  input: Unknown | Impulse<T>,
): input is Impulse<T>

isDerivedImpulse<T, Unknown = unknown>(
  scope: Scope,
  check: (value: unknown) => value is T,
  input: Unknown | Impulse<T>,
): input is Impulse<T>

A function that checks whether the input is a DerivedImpulse instance. If the check function is provided, it checks the Impulse's value to match the check function.

interface ReadableImpulse

An interface that defines the getValue method.

interface WritableImpulse

An interface that defines the setValue method.

type ReadonlyImpulse

A type alias for Impulse that does not have the Impulse#setValue method. It might be handy to store some value inside an Impulse, so the value change trigger a host component re-render only if the component reads the value from the Impulse.

interface ImpulseOptions

interface ImpulseOptions<T> {
  compare?: null | Compare<T>
}
  • [compare] is an optional Compare function that determines whether or not a new Impulse's value replaces the current one. In many cases specifying the function leads to better performance because it prevents unnecessary updates. But keep an eye on the balance between the performance and the complexity of the function - sometimes it might be better to replace the value without heavy comparisons.

interface UseScopedOptions

interface UseScopedOptions<T> {
  compare?: null | Compare<T>
}
  • [compare] is an optional Compare function that determines whether or not the factory result is different. If the factory result is different, a host component re-renders. In many cases specifying the function leads to better performance because it prevents unnecessary updates.

type Compare

type Compare<T> = (left: T, right: T, scope: Scope) => boolean

A function that compares two values and returns true if they are equal. Depending on the type of the values it might be reasonable to use a custom compare function such as shallow-equal or deep-equal.

ESLint

Want to see ESLint suggestions for the dependencies? Add the hook name to the ESLint rule override:

{
  "react-hooks/exhaustive-deps": [
    "error",
    {
      "additionalHooks": "useScoped(|Effect|LayoutEffect|Memo|Callback)"
    }
  ]
}