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

react-substate

v6.0.3

Published

Blazing-fast, centralized state management with auto-guaranteed, immutable state changes

Downloads

175

Readme

React Substate

Blazing-fast, centralized state management with auto-guaranteed, immutable state changes

Package Name NPM Version Minified and Zipped Size is 5.58 kB JavaScript Style Guide License CI Publish to NPM

Install

npm install react-substate [react react-dom]

Basic Example

import { createSubstate, createAction, useSubstate } from 'react-substate'

// Set up some sub-states
const substates = {
  test: createSubstate({ someField: 'the state' }),
  anotherTest: createSubstate(() => ({ foo: 'bar' })) // Use a generator function
}

// Set up some dispatchable Actions to modify state
const actions = {
  updateSomeField: createAction((draft, payload) => {
    draft.someField = payload // Sets `someField` in `draft` to the provided `payload`
  })
}

// Use it!
export const Component = () => {
  const test = useSubstate(substates.test)

  const handleClick = useCallback(() => {
    test.dispatch(actions.updateSomeField, 'the new state') // works
  }, [])

  return <button onClick={handleClick}>{test.value.someField}</button>
}

React Substate supports Redux DevTools

If you have the Redux DevTools extension installed in your browser, you'll be able to see changes driven by Substate creations and Action dispatches as they happen over time. The support is somewhat limited for now, but will only get better with time!

Migrating from 5.x to 6.x

The 6.0 release includes breaking changes to what useSubstate returns, as well as the removal of the Immer "patch" support that was previously exposed via usePatchEffect. The globalDispatch function has also been renamed to just dispatch.

useSubstate

Where you previously had something like:

const [test, dispatch] = useSubstate(substates.test)

Or as was often the case in larger applications:

const [test, dispatchTest] = useSubstate(substates.test)

You will instead use the clearer and less error-prone syntax of:

const test = useSubstate(substates.test)

To get the current value of a substate, use:

test.value

And to get the Substate-specific dispatch function, use:

test.dispatch(...)

If you still really want to destructure these into {value: test, dispatch: testDispatch} you can, however this is not the recommended approach.

useGlobalDispatch

Where you previously had something like:

const globalDispatch = useGlobalDispatch()

You will instead use:

const dispatch = useDispatch()

usePatchEffect

This hook has been removed and there is no planned replacement for it. If you still need its functionality, use v5.x instead.

TypeScript enhancements

The typing of React Substate is now better ar preventing users from doing the "wrong thing" by carrying forward types from Substate definitions all the way through to Actions verbatim and no longer widening types when additional properties are provided to either drafts or payloads.

Intro

React Substate boils down to three main parts:

Substates

This is how you store your application state. You can create new substates wherever you want, but it's often useful to define related groups of them together in the same file.

When you create a substate, what you get back is a "key" which is used later on to refer to the Substate in other functions of the library.

Substates are inexpensive, so you have the freedom to define them based on how you'd like to trigger re-renders within your application.

Few, large Substates will lead to heavier and more frequent re-renders, but can be useful in applications where even the most nested of components require a lot of data, or when the application is sufficiently small.

Many, small Substates generally leads to better-designed applications with fewer re-renders. The disadvantage of this approach is some additional legwork to adequately divide your application state into Substates.

Actions

Like other action/dispatch-driven frameworks, React Substate requires that state be updated through discrete Actions previously registered with the framework. These Actions can be created/registered at any time, but it is advisable to define them up front and all together in their own file(s).

What sets React Substate apart from other state management libraries is immutability. When working with your state inside of an Action, the object is automatically proxied by Immer to ensure that no matter how you manipulate the state inside of the Action, the result is an immutable state change. By convention, Immer refers to the proxied state object as a draft, and it's always the first parameter available inside of your Action functions.

An Action can be used to update any Substate. You have the flexibility to choose whether to write general-purpose Actions that can apply to multiple different Subsates with similar structures or very specific Actions that only make sense when called against a single Substate. It is often easier to debug an application when the Actions are specific and discrete, but this is not required by the framework. For example, a single, giant Action called doUpdate with a bunch of conditionals in it is possible, but most likely not a great idea.

Actions you create can later be passed to a dispatch function to cause your Substates to change, and ultimately your components to re-render. dispatch takes an action and a payload as arguments. The payload can be anything you might need to calculate the new state from inside your Action.

Hooks

React Substate's Hooks are what give you access to your state and changes to that state. A component can use as many useSubstate hooks as needed to obtain the data it needs to render.

In addition to giving back the current value of a Substate, useSubstate returns a dispatch function that can be called (with an action and payload) to update the value of that particular Substate.

Depending on your preference, you can also opt to use the general-purpose useDispatch hook instead of dealing with Substate-specific ones. useDispatch returns a function which takes three arguments instead of two: A Substate key, an Action key, and a payload. More on this in the examples below.

Examples

Basic TypeScript example

import { useCallback } from 'react'
import {
  createSubstate,
  createAction,
  useSubstate
} from 'react-substate'

interface Test {
  someField: string
}

const substates = {
  // By default, the type of the Substate will be inferred from the provided argument
  simple: createSubstate({foo: 'bar'})
  // A type hint can be provided to be more specific.
  test: createSubstate<Test>({someField: 'the state'})
}

const actions = {
  updateSomeField: createAction(
    // The Subtate's type can then also be used in the Action that modifies the Substate
    (draft: Test, payload: Test['someField']) => {
      draft.someField = payload // Will become "the new state"
    }
  )
}

export const Component = () => {
  const test = useSubstate(substates.test)

  const handleClick = useCallback(() => {
    test.dispatch(actions.updateSomeField, 'the new state') // works
    // test.dispatch(actions.updateSomeField, 123) <-- error: must pass a string
  }, [])

  return (
    <button onClick={handleClick}>{test.value.someField}</button>
  )
}

Multiple Substates, One Dispatcher

import { useCallback } from 'react'
import {
  createSubstate,
  createAction,
  useSubstate,
  useDispatch
} from 'react-substate'

const substates = {
  simple: createSubstate({foo: 'bar'})
  test: createSubstate({someField: 'the state'})
}

const actions = {
  updateFoo: createAction((draft, payload) => {
    draft.foo = payload
  }),
  updateSomeField: createAction(
    (draft, payload) => {
      draft.someField = payload
    }
  )
}

export const Component = () => {
  const simple = useSubstate(substates.simple)
  const test = useSubstate(substates.test)
  const dispatch = useDispatch()

  const handleClick = useCallback(() => {
    dispatch(substates.simple, actions.updateFoo, 'new foo!')
    dispatch(substates.test, actions.updateSomeField, 'the new state')
  }, [])

  return (
    <button onClick={handleClick}>
      {simple.value.foo} {test.value.someField}
    </button>
  )
}

Replacing the entire Substate value

import { useCallback } from 'react'
import { createSubstate, createAction, useSubstate } from 'react-substate'

const substates = {
  test: createSubstate({ someField: 'the state' })
}

const actions = {
  resetTest: createAction((_draft, _payload) => {
    // Just like Immer's `produce`, returning a value replaces the draft entirely
    return {
      someField: 'the brand new state'
    }
  })
}

export const Component = () => {
  const test = useSubstate(substates.test)

  const handleClick = useCallback(() => {
    test.dispatch(actions.resetTest, null)
  }, [])

  return <button onClick={handleClick}>{test.value.someField}</button>
}

Unit testing

import { render, screen } from '@testing-library/react'
import { createSubstate } from 'react-substate'

import { substates } from '../substates.js'
import { Component } from '../component.js'

describe('Cool unit tests', () => {
  it('works when given a specific value', () => {
    substates.test = createSubstate({ something: 'very specific' })

    render(<Component />)

    expect(screen.getByRole('button')).toHaveTextContent('very specific')
  })
})

API Reference

Functions

createSubstate

Creates and registers a new Substate with the given initial data. Returns a "key" for the Substate that can be passed to other functions like useSubstate or dispatch.

createAction

Registers a new dispatchable Action that modifies a Substate. Returns a "key" for the Action that can be passed to a dispatch function.

setDebugEnabled

Turns on/off logging of debug statements to the JavaScript console.

setDevToolsEnabled

Turns on/off logging of Substate changes to the Redux DevTools browser extension.

Hooks

useSubstate

Hook that allows a component to listen for changes to a Substate and receive a reference to a dispatch function that can be called to update that Substate. The return value is an object of the form { value: <obj>, dispatch: <fn> }.

useDispatch

Hook that returns a reference to a dispatch function that can be called to update any provided Substate without also listening for changes to any Substates.

Configuration

ImmerConfig

React Substate uses Immer under the covers to ensure state changes happen in an immutable way. Immer is left at its default behavior except for one exception: Auto-freezing is turned off by default to speed up performance.

If you want to turn this back on or configure any other aspects of Immer in your application, you can use the exported functions like so:

import { ImmerConfig } from 'react-substate'

ImmerConfig.setAutoFreeze(true)
ImmerConfig.useMapSet(true)

// etc.

Peer Dependencies

This module has peer dependencies on:

  • react version 16.14 (with hooks support) or higher.
  • react-dom version 16 or higher.

License

MIT © Harvtronix