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

oustate

v0.1.0

Published

πŸ‘€ Another React state management library

Downloads

56

Readme

Oustate

Build Code quality check Build Size

πŸ‘€ Another small, fast but robust ☝️ React state management library with aim 🎯 on simplicity and scalability in real world scenarios.

Still experimental / beta. Do not use it in productions yet! πŸ‘»

Based on React hooks api. Inspired by recoil and zustand while try to achieve the best from these two worlds.

Solving problems like the dreaded zombie child problem, react concurrency and context loss between mixed renderers with focus on re-renders reduction.

Install

yarn add oustate  # or npm i oustate

Quick Start

Simple state example live demo

import { createState, useStateValue } from 'oustate'

const userState = createState({ username: 'John', age: 30 })

const App = () => {
  const user = useStateValue(userState) // return all the user object
  return (
    <div
      onClick={() =>
        userState.set((user) => {
          user.age++
          return { ...user }
        })
      }>
      {user.age}
    </div>
  )
}

Simple state example with care about re-renders live demo

import { createState, useStateValue } from 'oustate'

const userState = createState({ username: 'John', age: 30 })

const App = () => {
  const userAge = useStateValue(userState, user => user.age) // return only user.age, so this component re-render only if user.age is changed.
    <div
      onClick={() =>
        userState.set((user) => {
          user.age++
          return { ...user }
        })
      }>
      {userAge}
    </div>
  )
}

Simple computed example live demo

Simple computed async example live demo

API

createState

creating basic atom state - it can be almost any value - object / atom / ...

Types

export interface StateOptions<T> {
  isSame?: IsSame<T>
  onSet?: (oldValue: T, setCallback: () => T) => T
}
/**
 * Creating of basic atom state.
 * @param defaultState - any value
 * @param options - optional options for state (isSame, onSet)
 * @returns AtomState
 */
export declare const createState: <T>(
  defaultState: StateInternal<T>,
  options?: StateOptions<StateInternal<T>> | undefined,
) => AtomState<StateInternal<T>>

Example:

import { createState } from 'oustate'

const defaultState = 2
const state = createState(defaultState, {
  // options are optional
  isSame: (prev, next) => true,
  onSet(oldValue, setCallback) {
    const newValue = setCallback()
    console.log(oldValue, newValue)
  },
})

// get state out of react scope
state.get()

// set new state
state.set(3)

// use state in react scope
const stateValue = useStateValue(state)
createStateFamily

same as createState but instead of returning AtomState, it returns function where first parameter (key) is unique state identifier and returns AtomState

Types

export interface StateOptions<T> {
  isSame?: IsSame<T>
  onSet?: (oldValue: T, setCallback: () => T) => T
}
/**
 * Create atom family state. It's same as createState but instead of return `AtomState` it returns `AtomFamily`.
 * `AtomFamily` is function that accepts `key` and returns `AtomState`.
 */
export declare const createStateFamily: <T>(
  defaultState: StateInternal<T>,
  options?: StateOptions<StateInternal<T>> | undefined,
) => AtomFamily<StateInternal<T>>

Example:

import { createStateFamily } from 'oustate'

const defaultState = 2
const state = createStateFamily(defaultState, {
  // options are optional
  isSame: (prev, next) => true,
  onSet(oldValue, setCallback) {
    const newValue = setCallback()
    console.log(oldValue, newValue)
    return newValue
  },
})

// get state out of react scope
state('some-key').get()

// set new state
state('some-key').set(3)

// use state in react scope
const stateValue = useStateValue(state('some-key'))
createComputed

computed state is a state that depends on other states or other computed states. It is recomputed when the states it depends on change. It can be also async

Types

interface GetSelectionOptionsBase {
  get: GetState
  abortSignal?: AbortSignal
  isCanceled: () => boolean
  key?: Key
}
export declare type GetSelectionOptions<T = unknown> = GetSelectionOptionsBase & T
/**
 * Computed state is a state that depends on other states or other computed states.
 * It is recomputed when the states it depends on change.
 * **It can be also async**.
 */
export declare const createComputed: <T>(
  getSelection: (options: GetSelectionOptions) => StateInternal<T> | Promise<StateInternal<T>>,
  options?: ComputedOptions<StateInternal<T>> | undefined,
) => ComputedState<StateInternal<T>>

Example:

import { createState, createComputed } from 'oustate'

const counterState = createState(0)
const userState = createState({ name: 'John', age: 20 })

// creating computed depends on counterState & userState
const counterPlusUserAgeState = createComputed(({ get }) => get(counterState) + get(userState, (user) => user.age))
// get state
await counterPlusUserAgeState.get()

// react scope
const counterPlusUser = useStateValue(counterPlusUserAgeState)

Note: Keep in mind when using useStateValue and async computed state, component need to be wrapped into the Suspense! For more control over computed loading states use useLoadableStateValue instead useStateValue

createComputedFamily

same as createComputed, but instead of returning ComputedState, it returns function where first parameter (key) is unique state identifier and returns ComputedState

Example:

import { createState, createComputed } from 'oustate'

const counterState = createState(0)
const userState = createState({ name: 'John', age: 20 })

// creating computed depends on counterState & userState
const counterPlusUserAgeState = createComputedFamily(({ get }) => get(counterState) + get(userState, (user) => user.age))
// get state
await counterPlusUserAgeState('key').get()

// react scope
const counterPlusUser = useStateValue(counterPlusUserAgeState('key'))
createSlice

it's just helper function - slice wrapped around createComputed. There are scenarios when need to slice 1 state in same way in multiple components (const userAge = useStateValue(userState, user => user.age) ), instead of writing same logic multiple times in react scope, createSlice helps to bring it to the global scope (const userAgeState = createSlice(userState, user => user.age) ).

Example:

import { createState, createSlice } from 'oustate'

const userState = createState({ name: 'John', age: 20 })

const userAgeState = createSlice(userState, (user) => user.age)
// get state
await userAgeState.get()

// react scope
const counterPlusUser = useStateValue(userAgeState)

Passing functions into the state (like setting state) is not recommended.

Note: State need to be used in global js context (🀫 it can be used also in React context, but carefully!)

Hooks:

After state is created you can easily use it with useStateValue, useLoadableStateValue or useCachedStateValue hooks.

useStateValue

hook for getting state value - when async computed is used - need wrap component into the suspense.

useLoadableStateValue

hook for getting state value but with more control over loading state - component don't need to be wrapped into the suspense

useCachedStateValue

hook for getting state with caching control - it's useful for async computed states - when on first load it went to the suspense, but on second change it will returns loading state + old state

babel-plugin

oustate-babel-transform-plugin - For lazy persons πŸ₯±πŸ˜΄

  • there is also babel plugin to avoid re-renders without defining areEqual function manually - it will generates compare function automatically. if there is manually defined compare function - plugin will ignore it. if there is defined custom slice selector - plugin will respect it.

for example something like this:

import { useStateValue } from 'oustate'

// without defining slice and return just atom value or define compare function manually - component will re-render on each time when any property from userState will change
const { username } = useStateValue(userState)
const { name } = useStateValue(someState, (state) => ({ name: state.notNameProperty }))

will be automatically converted to something like this:

import { useStateValue } from 'oustate'

// function below is automatically generated by oustate-babel-transform-plugin
function isSameCompare1(prev, next) {
  if (prev.username !== next.username) {
    return false
  }
  return true
}
function isSameCompare2(prev, next) {
  if (prev.name !== next.name) {
    return false
  }
  return true
}

const { username } = useStateValue(userState, undefined, isSameCompare1) // component only re-render when username is changed.
const { name } = useStateValue(someState, (state) => ({ name: state.notNameProperty }), isSameCompare2) //  useful when creating new references from slice selector.
install babel plugin:
yarn add -D oustate-babel-transform-plugin  # or npm i -D oustate-babel-transform-plugin
using babel plugin

update plugins in your babel.config.js or .babelrc file

module.exports = {
  plugins: ['oustate-babel-transform-plugin'],
}

Plugin works only for oustate hooks, when directly access object properties or array properties:

  • ObjectPattern (const {some} = useStateValue(state))
  • ArrayPattern (const [some, value] = useStateValue(state))

here are cases which will be not transformed:

const someState = useStateValue(state)
const { value } = someState // transform will be ignored because it's not called directly from useStateValue hook.
const someState = useStateValue(state)
someState.value // transform will be ignored because it's not called directly from useStateValue hook.

Slicing guide

to avoid re-renders, state can be sliced in react scope, computed scope.

  • react-scope example how to slice state and avoid re-renders when not used values are changed:

    const counter = useStateValue(userState, (state) => state.counter) // it will pick the counter property from userState object, so only if counter from state will change, this component will re-render

    example when slice will not work:

    const { counter } = useStateValue(userState, (state) => ({ counter: state.counter })) // here is the issue, because while creating new slice, it also create new object reference `({counter:something})`

    In this example component will re-render on each userState change, because instead of picking state data directly, it's added to the new object reference. Creating new references with slice / selector will always cause re-renders. There is fix around it and it's third parameter in hooks - equality check. That's why babel transform plugin exist. Also there can be used third party libraries to equality check like react-fast-compare.

    example with equality check:

    const isSame = (prev, next) => {
      if (prev.counter !== next.counter) {
        return false
      }
      return true
    }
    
    const { counter } = useStateValue(userState, (state) => ({ counter: state.counter }), isSame) // equality function comparing always object from selector, if provided, otherwise state.
  • computed-scope example how to slice state in computed and avoid re-renders & computed calls when not used values are changed:

    const computedCounter = createComputed(({ get }) => {
      return get(userState, (state) => state.counter) // slicing the state in computed
    })

Keep in mind that using slices / selectors in all hooks don't need to be memoized! Selectors no longer need to be memoized

πŸ‘‹ Welcome back inline functions πŸ‘‹

Well tested, written in typescript.

This library is just playing around with possible solutions how to create react states, so if you can look at the code & give some feedback - positive or negative, I will appreciate it! πŸ€—