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

@oleksii-pavlov/desirable

v1.1.1

Published

State manager for vanilla js application and not only

Downloads

5

Readme

Desirable - effective solution for state management

Basic example

import { createStore } from '@oleksii-pavlov/desirable'

const initialStore = {
  counter: 0
}

const counterStore = createStore(initialStore, (state) => ({
  increment: () => state.counter++,
  decrement: () => state.counter--,
  plus: (amount: number) => state.counter += amount,
  minus: (amount: number) => state.counter -= amount,
}))

counterStore.subscribe((state) => console.log(state))

counterStore.reducers.increment()
// { counter: 1 }
counterStore.reducers.decrement()
// { counter: 0 }

Comparing with Redux Toolkit and Zustand

Desirable requires less boilerplate than Redux Toolkit. In comparison with Zustand, it has some pluses like dividing data and reducers saving API effectiveness and createStore() does not require explicit typing to write reducers (State type is got from initialState). Also, Desirable both gives an access to the core processes of the state management (initialization, resetting, subscription clearing) and allows not to waste time writing boilerplate. Except of it, Desirable is not only for React - you can use wherever you need and for React, there is an especial module (@oleksii-pavlov/desirable/react)

Create a store

import { createStore } from '@oleksii-pavlov/desirable'

// define initial state
const initialState = {
  counter: 0
}

// create store by passing initial state and adding reducers
const counterStore = createStore(initialState, (state) => ({
  increment: () => state.counter++,
  decrement: () => state.counter--,
  updateValue: (value: number) => state.counter = value,
}))

Reducers. How to update state?

createStore(initialState, (state) => ({
  increment: () => state.counter++,
  decrement: () => state.counter--,
  updateValue: (value: number) => state.counter = value,
}))

As you can see, to create reducers after initialState you pass to createStore() a callback which takes an argument state are returns an object with methods which are reducers and they can update state. These methods take payload arguments that can be used to update state.

After creation, you can access your reducers by store.reducers object that contains the set of reducers. In this example, you would use store.reducers.increment()

Subscribe on state changes. How to listen to state changes?


After you learned how to create and update the state, we need to learn how to subscribe on state changes. Here is an example:

import { createStore } from '@oleksii-pavlov/desirable'

const initialState = {
  counter: 0
}

const counterStore = createStore(initialState, (state) => ({
  increment: () => state.counter++,
  decrement: () => state.counter--,
}))

counterStore.subscribe((state) => console.log('Counter value:', state.counter))

counterStore.reducers.increment() // Counter value: 1
counterStore.reducers.increment() // Counter value: 2

Using store.subscribe() you can subscribe on a store and listen to its state changes. store.subscribe() takes a callback as an argument. This callback has one parameter state which gives an access to the actual state of the store.

Subscribe on specific slice. How avoid unnecessary callback calls?

Sometimes we need to do something when some specific pieces of state updated only. For example, we can have a store that contains data about the user account and it has login and profile description. We want to update description field after the description is updated but we do not need to rerender login section. We want to make them independent but inside of a single store. For that, we can subscribe on a specific slice:

const initialState = {
  login: 'Oleksii',
  description: 'The Frontend developer from Ukraine'
}

const accountStore = createStore(initialState, (state) => ({
  updateLogin: (login: string) => state.login = login,
  updateDescription: (description: string) => state.description = description,
}))

// subscribe on changes of "description" slice
accountStore.on(state => state.description).subscribe(state => {
  console.log('Description updated')
})

accountStore.reducers.updateDescription('The Fullstack developer from Ukraine')
// console: 'Description updated'
accountStore.reducers.updateLogin('Oleksii Pavlov')
// nothing happened

Here you can see a new method of the store - store.on(). It takes selector callback that takes state and returns specific slice that we want to listen to. store.on() method return Subscribable object. That means that we can call its subscribe() method and it works the same way as store.subscribe(). In this example, we subscribed "description" value. We also can subscribe on object and each time it is changed (its properties are changed) the callback will be called. Objects are compared recursively so even changes of nested objects are listened.

Advice: if you want to subscribe a few specific slices in the same state, you could think of splitting the store to some small stores in case it is possible.

Unsubscribe. How to stop listening changes?

Sometimes we need to stop listening to store changes. For that, you need to use unsubscribe() function. The same as in most of state managers, this function is returned by store.subscribe() and by store.on(selector).subscribe() as well:

import { createStore } from '@oleksii-pavlov/desirable'

// initial state
const initialState = {
  counter: 0
}

const counterStore = createStore(initialState, (state) => ({
  increment: () => state.counter++,
  decrement: () => state.counter--,
}))

// subscribe
const unsubscribe = counterStore.subscribe((state) => console.log('Counter value:', state.counter))

counterStore.reducers.increment() // Counter value: 1
counterStore.reducers.increment() // Counter value: 2

// stop listening and writing in console
unsubscribe()

counterStore.reducers.increment() // console has not been changed
counterStore.reducers.increment() // console has not been changed

Get state. How to get state not by subscription?

It happens that we need to get state in the moment only once, not subscribe on it. For these cases, there is a method store.getState(). It returns the current state of the store and does not subscribes on changes. Also, this method allows to use data from different stores at the same time:

// stores/account-store.ts
import { createStore } from '@oleksii-pavlov/desirable'

const initialState = {
  login: 'Luiza',
  followers: 10
}

export const accountStore = createStore(initialState, (state) => ({
  updateLogin: (login: string) => state.login = login,
  updateFollowers: (followers: number) => state.followers = followers,
}))
// stores/language-store.ts
import { createStore } from '@oleksii-pavlov/desirable'

const initialState = {
  language: 'en'
}

export const languageStore = createStore(initialState, (state) => ({
  updateLanguage: (language: string) => state.language = language,
}))
// features/send-email.ts
import { accountStore } from '../stores/account-store'
import { languageStore } from '../stores/language-store'

export function sendLetterByEmail() {
  const accountState = accountStore.getState() // { login: 'Luiza', followers: 10 }
  const languageState = languageStore.getState() // { language: 'en' }

  // do some code with both states
}

Other methods to work with stores

  • store.init() triggers subscriptions call without updating the data
  • store.resetState() updates state by setting it to initialState given to createStore()
  • store.clearAllSubscriptions() removes all subscriptions by calling unsubscribe() for each subscription

React version

If you have a React application, you might be more comfortable to use React version of this state manager. For that, simply import createStore() from @oleksii-pavlov/desirable/react. This createStore() has the same API but it returns object with one more method useSelector. You need to use it this way:

// component.tsx
import { useCounterSelector, increment, decrement } from './counter-store'

const Component = () => {
  const counter = useCounterSelector((state) => state.counter)

  return (
    <>
      <div>Counter: {counter}</div>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </>
  )
} 
// counter-store.ts
import { createStore } from '@oleksii-pavlov/desirable/react'

const initialState = {
  counter: 0
}

export const counterStore = createStore(initialState, (state) => ({
  increment: () => state.counter++,
  decrement: () => state.counter--
}))

export const useCounterSelector = counterStore.useSelector
export const { increment, decrement } = counterStore.reducers

You can see that the createStore() returns store with useSelector method. It is highly recommended to rename it to something unique not to have conflicts of names in your code. Then you can use this hook in your components: it takes Selector (as store.on() method) and returns selected slice of the state. The behavior of this returned value is the same as useState() returned value. Or you can compare this useSelector() with React-Redux useSelector() that you might have used with Redux Toolkit.