react-sweety
TypeScript icon, indicating that this package has built-in type declarations

3.0.2 • Public • Published

THE LIBRARY HAS CHANGED NPM NAME FROM react-sweety TO react-impulse.

react-sweety

codecov known vulnerabilities minified + gzip dependency count types

The clean and natural React state management.

# with yarn
yarn add react-sweety

# with npm
npm install react-sweety

Quick start

Sweety is a box holding any value you want, even another Sweety! All watched components that execute the Sweety#getState during the rendering phase enqueue re-render whenever the Sweety instance's state updates.

import { Sweety, watch } from "react-sweety"

const Input: React.FC<{
  type: "email" | "password"
  value: Sweety<string>
}> = watch(({ type, value }) => (
  <input
    type={type}
    value={value.getState()}
    onChange={(event) => value.setState(event.target.value)}
  />
))

const Checkbox: React.FC<{
  checked: Sweety<boolean>
  children: React.ReactNode
}> = watch(({ checked, children }) => (
  <label>
    <input
      type="checkbox"
      checked={checked.getState()}
      onChange={(event) => checked.setState(event.target.checked)}
    />

    {children}
  </label>
))

Once created, Sweety instances can travel thru your components, where you can set and get their states:

import { useSweety, watch } from "react-sweety"

const SignUp: React.FC = watch(() => {
  const username = useSweety("")
  const password = useSweety("")
  const isAgreeWithTerms = useSweety(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.getState()}
        onClick={() => {
          api.submitSignUpRequest({
            username: username.getState(),
            password: password.getState(),
          })
        }}
      >
        Sign Up
      </button>
    </form>
  )
})

Demos

API

A core piece of the library is the Sweety 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 Sweety instances can use them as values.

Sweety.of

Sweety.of<T>(
  initialState: T,
  compare?: null | Compare<T>
): Sweety<T>

A static method that creates a new Sweety instance.

  • initialState is the initial state.
  • [compare] is an optional Compare function applied as Sweety#compare. When not defined or null then Object.is applies as a fallback.

💡 The useSweety hook helps to create and store a Sweety instance inside a React component.

Sweety#getState

Sweety<T>#getState(): T
Sweety<T>#getState<R>(select: (state: T) => R): R

A Sweety instance's method that returns the current state.

  • [select] is an optional function that applies to the current state before returning.
const count = Sweety.of(3)

count.getState() // === 3
count.getState((x) => x > 0) // === true

Sweety#setState

Sweety<T>#setState(
  stateOrTransform: React.SetStateAction<T>,
  compare?: null | Compare<T>
): void

A Sweety instance's method to update the state. All listeners registered via the Sweety#subscribe method execute whenever the instance's state updates.

  • stateOrTransform is the new state or a function that transforms the current state into the new state.
  • [compare] is an optional Compare function applied for this call only. When not defined the Sweety#compare function of the instance will be used. When null the Object.is function applies to compare the states.
const isActive = Sweety.of(false)

isActive.setState((x) => !x)
isActive.getState() // true

isActive.setState(false)
isActive.getState() // false

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

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

Sweety#clone

Sweety<T>#clone(
  transform?: (state: T) => T,
  compare?: null | Compare<T>
): Sweety<T>

A Sweety instance's method for cloning a Sweety instance.

  • [transform] is an optional function that applies to the current state before cloning. It might be handy when cloning a state that contains mutable values.
  • [compare] is an optional Compare function applied as Sweety#compare. When not defined, it uses the Sweety#compare function from the origin. When null the Object.is function applies to compare the states.
const immutable = Sweety.of({
  count: 0,
})
const cloneOfImmutable = immutable.clone()

const mutable = Sweety.of({
  counters: [Sweety.of(0), Sweety.of(1)],
})
const cloneOfMutable = mutable.clone(({ counters }) => ({
  counters: counters.map((counter) => counter.clone()),
}))

Sweety#compare

Sweety<T>#compare: Compare<T>

The Compare function compares the state of a Sweety instance with the new state given via Sweety#setState. Whenever the function returns true, neither the state change nor it notifies the listeners subscribed via Sweety#subscribe.

Sweety#subscribe

Sweety<T>#subscribe(listener: VoidFunction): VoidFunction

A Sweety instance's method that subscribes to the state's updates caused by calling Sweety#setState. Returns a cleanup function that unsubscribes the listener.

  • listener is a function that subscribes to the updates.
const count = Sweety.of(0)
const unsubscribe = count.subscribe(() => {
  console.log("The count is %d", count.getState())
})

count.setState(10) // console: "The count is 10"

unsubscribe()
count.setState(20) // ...

💬 You'd like to avoid using the method in your application because it's been designed for convenient use in the exposed hooks and the watch HOC.

watch

function watch<TProps>(component: React.FC<TProps>): React.FC<TProps>

The watch function creates a React component that subscribes to all Sweety instances calling the Sweety#getState method during the rendering phase of the component.

The Counter component below enqueues a re-render whenever the count's state changes, for instance, when the Counter's button clicks:

const Counter: React.FC<{
  count: Sweety<number>
}> = watch(({ count }) => (
  <button onClick={() => count.setState((x) => x + 1)}>
    {count.getState()}
  </button>
))

But if a component defines a Sweety instance, passes it thru, or calls the Sweety#getState method outside of the rendering phase (ex: as part of event listeners handlers), then it does not subscribe to the Sweety instances changes.

Here the SumOfTwo component defines two Sweety instances, passes them further to the Counters components, and calls Sweety#getState inside the button.onClick handler. It is optional to use the watch function in that case:

const SumOfTwo: React.FC = () => {
  const firstCounter = useSweety(0)
  const secondCounter = useSweety(0)

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

      <button
        onClick={() => {
          const sum = firstCounter.getState() + secondCounter.getState()

          console.log("Sum of two is %d", sum)

          firstCounter.setState(0)
          secondCounter.setState(0)
        }}
      >
        Save and reset
      </button>
    </div>
  )
}

With or without wrapping the component around the watch HOC, The SumOfTwo component will never re-render due to either firstCounter or secondCounter updates, but still, it can read and write their states inside the onClick listener.

watch.memo

Alias for

React.memo(watch(/* */))
// equals to
watch.memo(/* */)

watch.forwardRef

Alias for

React.forwardRef(watch(/* */))
// equals to
watch.forwardRef(/* */)

watch.memo.forwardRef and watch.forwardRef.memo

Aliases for

React.memo(React.forwardRef(watch(/* */)))
// equals to
watch.memo.forwardRef(/* */)
watch.forwardRef.memo(/* */)

useSweety

function useSweety<T>(
  initialState: T | (() => T),
  compare?: null | Compare<T>
): Sweety<T>
  • initialState argument is the state used during the initial render. If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render.
  • [compare] is an optional Compare function applied as Sweety#compare. When not defined or null then Object.is applies as a fallback.

A hook that initiates a stable (never changing) Sweety instance.

💬 The initial state is disregarded during subsequent re-renders.

useWatchSweety

function useWatchSweety<T>(
  watcher: () => T,
  compare?: null | Compare<T>
): T
  • watcher is a function that subscribes to all Sweety instances calling the Sweety#getState method inside the function.
  • [compare] is an optional Compare function. When not defined or null then Object.is applies as a fallback.

The useWatchSweety hook is an alternative to the watch function. It executes the watcher function whenever any of the involved Sweety instances' state update but enqueues a re-render only when the resulting value is different from the previous.

Custom hooks can use useWatchSweety for reading and transforming the Sweety instances' states, so the host component doesn't need to wrap around the watch HOC:

const useSumAllAndMultiply = ({
  multiplier,
  counts,
}: {
  multiplier: Sweety<number>
  counts: Sweety<Array<Sweety<number>>>
}): number => {
  return useWatchSweety(() => {
    const sumAll = counts
      .getState()
      .map((count) => count.getState())
      .reduce((acc, x) => acc + x, 0)

    return multiplier.getState() * sumAll
  })
}

Components can scope watched Sweety instances to reduce re-rendering:

const Challenge: React.FC = () => {
  const count = useSweety(0)
  // the component re-renders only once when the `count` is greater than 5
  const isMoreThanFive = useWatchSweety(() => count.getState() > 5)

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

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

💬 The watcher function is only for reading the Sweety instances' states. It should never call Sweety.of, Sweety#clone, Sweety#setState, or Sweety#subscribe methods inside.

💡 It is recommended to memoize the watcher function with React.useCallback for better performance.

💡 Keep in mind that the watcher function acts as a "reader" so you'd like to avoid heavy calculations inside it. Sometimes it might be a good idea to pass a watcher 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.

useSweetyMemo

function useSweetyMemo<T>(
  factory: () => T,
  dependencies: ReadonlyArray<unknown> | undefined,
): T
  • factory is a function 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 a Sweety version of the React.useMemo hook. During the factory execution, all the Sweety instances that call the Sweety#getState method become phantom dependencies of the hook.

Click here to learn more about the phantom dependencies.

The factory runs again whenever any dependency or a state of any phantom dependency changes:

const useCalcSum = (left: number, right: Sweety<number>): number => {
  // the factory runs whenever:
  // 1. `left` changes
  // 2. `right` changes (new `Sweety` instance)
  // 3. `right.getState()` changes (`right` mutates)
  return useSweetyMemo(() => {
    return left + right.getState()
  }, [left, right])
}

The phantom dependencies might be different per factory call. If a Sweety instance does not call the Sweety#getState method, it does not become a phantom dependency:

const useCalcSum = (left: number, right: Sweety<number>): number => {
  // the factory runs when either:
  //
  // `left` > 0:
  //   1. `left` changes
  //   2. `right` changes (new `Sweety` instance)
  //   3. `right.getState()` changes (`right` mutates)
  //
  // OR
  //
  // `left` <= 0:
  //   1. `left` changes
  //   2. `right` changes (new `Sweety` instance)
  return useSweetyEffect(() => {
    if (left > 0) {
      return left + right.getState()
    }

    return left
  }, [left, right])
}

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

{
  "react-hooks/exhaustive-deps": [
    "error",
    {
      "additionalHooks": "(useSweetyEffect|useSweetyLayoutEffect|useSweetyMemo)"
    }
  ]
}

useSweetyEffect

function useSweetyEffect(
  effect: () => (void | VoidFunction),
  dependencies?: ReadonlyArray<unknown>,
): void
  • effect is a function that 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 a Sweety version of the React.useEffect hook. During the effect execution, all the Sweety instances that call the Sweety#getState method become phantom dependencies of the hook.

Click here to learn more about the phantom dependencies.

The effect runs again whenever any dependency or a state of any phantom dependency changes:

const usePrintSum = (left: number, right: Sweety<number>): void => {
  // the effect runs whenever:
  // 1. `left` changes
  // 2. `right` changes (new `Sweety` instance)
  // 3. `right.getState()` changes (`right` mutates)
  useSweetyEffect(() => {
    console.log("sum is %d", left + right.getState())
  }, [left, right])
}

The phantom dependencies might be different per effect call. If a Sweety instance does not call the Sweety#getState method, it does not become a phantom dependency:

const usePrintSum = (left: number, right: Sweety<number>): void => {
  // the effect runs when either:
  //
  // `left` > 0:
  //   1. `left` changes
  //   2. `right` changes (new `Sweety` instance)
  //   3. `right.getState()` changes (`right` mutates)
  //
  // OR
  //
  // `left` <= 0:
  //   1. `left` changes
  //   2. `right` changes (new `Sweety` instance)
  useSweetyEffect(() => {
    if (left > 0) {
      console.log("sum is %d", left + right.getState())
    }
  }, [left, right])
}

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

{
  "react-hooks/exhaustive-deps": [
    "error",
    {
      "additionalHooks": "(useSweetyEffect|useSweetyLayoutEffect|useSweetyMemo)"
    }
  ]
}

useSweetyLayoutEffect

The hook is a Sweety version of the React.useLayoutEffect hook. Acts similar way as useSweetyEffect.

useSweetyInsertionEffect

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

const usePrintSum = (left: number, right: Sweety<number>): void => {
  const rightState = useSweetyState(right)

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

useSweetyState

function useSweetyState<T>(sweety: Sweety<T>): T

A hook that subscribes to the sweety changes and returns the current state.

  • sweety is a Sweety instance.
const Input: React.FC<{
  value: Sweety<string>
}> = ({ value }) => {
  const text = useSweetyState(value)

  return (
    <input
      type="text"
      value={text}
      onChange={(event) => value.setState(event.target.value)}
    />
  )
}

batch

function batch(execute: VoidFunction): void

The batch function is a helper to optimize multiple Sweety updates.

  • execute is a function that executes multiple Sweety#setState calls at ones.
const SumOfTwo: React.FC<{
  left: Sweety<number>
  right: Sweety<number>
}> = watch(({ left, right }) => (
  <div>
    <span>Sum is: {left.getState() + right.getState()}</span>

    <button
      onClick={() => {
        // enqueues 1 re-render instead of 2 🎉
        batch(() => {
          left.setState(0)
          right.setState(0)
        })
      }}
    >
      Reset
    </button>
  </div>
))

Compare

type Compare<T> = (left: T, right: T) => 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.

Publish

Here are scripts you want to run for publishing a new version to NPM:

  1. npm version {version} ex: npm version 1.0.0-beta.1
  2. npm run build
  3. npm publish --tag {tag} ex: npm publish --tag beta --tag latest
  4. git push
  5. git push --tags

Package Sidebar

Install

npm i react-sweety

Weekly Downloads

4

Version

3.0.2

License

BSD-3-Clause

Unpacked Size

127 kB

Total Files

8

Last publish

Collaborators

  • owanturist