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

usehyperstate

v1.0.2

Published

React custom hook for state management a-la Hyperapp

Downloads

2

Readme

useHyperState

React custom hook for state management a-la Hyperapp.

Quick Example

import React from 'react'
import ReactDOM from 'react-dom/client'
import useHyperState from 'usehyperstate'

const Increment = x => x + 1
const Decrement = x => x - 1

function App(props) {
  const [state, _] = useHyperState({init: 0})
  return (
    <main>
      <h1>{state}</h1>
      <button onClick={_(Decrement)}>-</button>
      <button onClick={_(Increment)}>+</button>
    </main>
  )
}

ReactDOM.createRoot( 
  document.querySelector('#root')
).render(<App />)

Introduction

This custom hook brings Hyperapp-style state management to React. It is particularly useful if your state is complicated enough that you're already using useReducer or redux, but you find it cumbersome to manage state-transitions with assocated side effects.

And if you already know and love Hyperapp, this hook will allow you to reuse all your actions, effects and subscriptions for hyperapp in react. They should be 100% compatible since I actually copied the relevant parts of hyperapp's code verbatim over (There may be some edge-case issues with React's synthetic events I don't know about)

Basic usage

useHyperState({init: initializer }) takes a configuration object as its only argument. The only required property of the configuration object is init:. init: should be either:

  • The initial state (which should not be an array, but can be an object, string or number)
  • [initialState, ...effects[]] in order to run some effects immediately on initialization
  • An Action, or a Bound action, which should not depend on the current state since there is no current state at this point

useHyperState(...) returns a tuple of [currentState, makeHandler] for use in the JSX of your component.

makeHandler (which I typically name as simply _ creates an event handler from an Action or Bound Action. E.g:

<button onClick={_(SomeAction)}>Click me</button>

So that when the event occurs, the given action will be dispatched, with the event object as payload.

State management with useHyperState

Actions

If you already know useReducer or redux, you already understand the basic pattern. However, unlike with those tools, there is no "reducer". Or rather, each action is it's own reducer. An Action is a function which takes the current state, and returns a new state.

const ToggleHints = state => ({...state, showHints: !state.showHints})

When an action is dispatched, the new state will be calculated based on the current state, which will cause a rerender with the new state as current.

An action may take a second argument (often called the "payload"). When actions are dispatched from events in the DOM, the payload will be the event object.

const SetName = (state, event) => ({...state, name: event.target.value})
...
<input type="text" value={state.name} onInput={_(SetName)} />

But actions can also be dispatched as "Bound Actions" - an action-payload-tuple where the given payload overrides the default.

const IncrementFooBy = (state, amount) => ({...state, foo: state.foo + amount})
...
<button onClick={_([IncrementFooBy, 3])}>Foo + 3</button>

Instead of returning a state, an action may return another action, or bound action, which will be dispatched instead. This allows for some convenient "switching actions":

const HandleKeypress = (state, ev) => 
    ev.key === 'ArrowUp'    ? MoveUp
  : ev.key === 'ArrowDown'  ? MoveDown
  : ev.key === 'ArrowLeft'  ? MoveLeft
  : ev.key === 'ArrowRight' ? MoveRight
  : state // returning state is effectively a no-op

Effects

Actions should be pure, i e they should be pure calculations without causing any side effects. They should not "do" anything. So anything you might wnat to do in relation to state changes should be encapsulated in its own function.

const jumpScareEffect = () => { alert('Boo!') }

Instead of returning the new state, or returning an action, an action may return an array where the new state is the first item, and the rest are effects.

const ScaryToggle = (state) => [
  {...state, fear: !state.fear},
  jumpScareEffect
]

This will cause the effect to run after the state has been updated (but before a new render).

Most often you want reusable effects, so they can take parameters (as second argument). In that case, an effect can be given as a function-payload-tuple:

const alert = (_, message) => {alert(message)}
const ScaryToggle = (state) => [
  {...state, fear: !state.fear},
  [alert, 'Boo!']
]

falsy values are treated as no-op effects, so that you can use logical expressions in your actions to conveniently decide wether or not to run an effect.

Some effects need to "call back" when they are done, such as when you are fetching something from a server. For this reason, effects are called with a dispatch function as the first argument. dispatch is used to dispatch actions.

const getJSON = (dispatch, opts) => fetch(opts.url)
  .then(response => response.json())
  .then(data => dispatch(opts.OnResponse, data)

const FetchData = state => [
  {...state, fetching: true},
  [getJSON, {url: 'http://example.com/api/data', OnResponse: GotData}]
]

const GotData = (state, data) => ({
  ...state,
  fetching: false,
  data,
})

Subscriptions

When there are things you want to "listen to" (window-events, setInterval, websockets et c), subscriptions are the thing to reach for.

Effectively, a subscription is a function that sets up the listening part, and returns a function that stops the listening. Like effects, subscriptions get a dispatch and options object as arguments, so they can call back when things happen.

const onResize = (dispatch, opts) => {
  // set up listening:
  const handler = () => dispatch(opts.OnResize)
  window.addEventListener('resize', handler)
  return () => {
    //tear down listening:
    window.removeEventListener('resize', handler)
  }
}

So that's how you define a subscription - but how do you use it? You pass a subscriptions: property to the useHyperState config object. The value of subscriptions should be a function which takes the current state as argument, and returns an array of all the subscriptions should be live given the current state:


const [state, _] = useHyperState({
  init: initialState,
  // typically you wouldn't inline the subscription function here
  // but have it defined outside the component definition
  // only shown here for illustrative purposes
  subscriptions: state => [
    [onResize, {OnResize: HandleResize}],
    state.timerRunning && [onInterval, {time: state.timerInterval, action: TimerTick}]
  ]
})

Each time the state is changed, this list is recalculated. The subscriptions which are no longer in the list are stopped. New subscriptions are started. And any subscriptions whose options have changed are restarted with the new options.

Notice how subscriptions are given as function-options-tuples, like effects. Also notice how we can conditionally activate subscriptions based on state.

More

The config object for useHyperState also supports "Augmented dispatching" (see hyperapp docs) through the dispatch property. It works the same way as in Hyperapp, and allows you to attach debug-tooling.

To gain a deeper understanding I recommend looking through the Hyperapp docs, and in particular the Hyperapp tutorial - just ignore anything pertaining to the view. The rest is compatible.