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

immer-store

v0.0.37

Published

Functional actions

Downloads

18

Readme

immer-store

Project for article

This project was created related to this article. Please have a read to see what it is all about.

!NOTE This project is not officially released, as it would require more testing and a maintainer.

Motivation

With the success of Immer there is no doubt that developers has no problems writing pure code in an impure API. The mutation API of JavaScript is straight forward and expressive, but the default result is impure, going against the immutable model favoured by React. With immer-store we allow Immer to take even more control and basically gets rid of reducers, dispatching and action creators.

Instead of having to write this:

// action creator
const receiveProducts = (products) => ({
  type: RECEIVE_PRODUCTS,
  products
})

// thunk
const getProducts = () => async (dispatch) => {
  dispatch(receiveProducts(await api.getProducts()))
}

// reducer
const productsById = produce((draft, action) => {
  switch (action.type) {
    case RECEIVE_PRODUCTS:
      action.products.forEach((product) => {
        draft[product.id] = product
      })
      return
  }
})

You can just write this:

const getProducts = ({ state }, payload) => {
  const products = await api.getProducts()

  products.forEach((product) => {
    state.products[product.id] = product
  })
}

Everything is still immutable.

How does it work?

immer-store takes inspiration from the "api"-less API of overmindjs. The codebase is rather small and commented, so you can take a dive into that. A quick summary though:

  • With a combination of chosen API and Proxies immer-store exposes a state object to your actions that produces Immer drafts under the hood. The concept of a draft is completely hidden from you. You only think actions and state
  • It supports changing state asynchronously in your actions
  • Because immer-store exposes an action API it also has access to the execution of the action and access to state. That means it is able to batch up mutations and notify components at optimal times to render
  • Instead of using selectors to expose state to components the useState hook tracks automatically what state you access and subscribes to it. That means there is no value comparison in every single hook on every mutation, but immer-store tells specifically what components needs to update related to matching batched set of mutations
  • immer-store has a concept of effects which is a simple injection mechanism allowing you to separate generic code from your application logic. It also simplifies testing
  • The library is written in Typescript and has excellent support for typing with minimal effort

Get started

import React from 'react'
import { render } from 'react-dom'
import { createStore, Provider, useState, useActions } from 'immer-store'

const store = createStore({
  state: {
    title: ''
  },
  actions: {
    changeTitle: ({ state }, title) => {
      state.title = title
    }
  }
})

function App() {
  const state = useState()
  const actions = useActions()

  return (
    <div>
      <h1>{state.title}</h1>
      <button onClick={() => actions.changeTitle('New Title')}>
        Change title
      </button>
    </div>
  )
}

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector('#app')
)

Scaling up

As your application grows you want to separate things a bit more.

// store/index.js
import * as home from '../pages/home'
import * as issues from '../pages/issues'

export const config = {
  state: {
    home: home.state,
    issues: issues.state
  },
  actions: {
    home: home.actions,
    issues: issues.actions
  }
}

// index.js
import { createStore } from 'immer-store'
import { config } from './store'

const store = createStore(config)

This structure ensures that you can split up your state and actions into different domains. It also separates the definition of your application from the instantiation of it. That means you can easily reuse the definition multiple times for testing purposes or server side rendering.

Using effects

Instead of importing 3rd party libraries and writing code with browser side effects etc. immer-store allows you to separate it from your application logic in the actions. This creates a cleaner codebase and you get several other benefits:

  1. All the code in your actions will be domain specific, no low level generic APIs
  2. Your actions will have less code and you avoid leaking out things like URLs, types etc.
  3. You decouple the underlying tool from its usage, meaning that you can replace it at any time without changing your application logic
  4. You can more easily expand the functionality of an effect. For example you want to introduce caching or a base URL to an HTTP effect
  5. You can lazy-load the effect, reducing the initial payload of the app

So you decide what you application needs and then an effect will provide it:

export const config = {
  state: {...},
  actions: {...},
  effects: {
    storage: {
      get: (key) => {
        const value = localStorage.getItem(key)

        return typeof value === 'string' ? JSON.parse(value) : value
      },
      set: (key, value) => {
        localStorage.setItem(key, JSON.stringify(value))
      }
    },
    http: {
      get: async (url) => {
        const response = fetch(url)

        return response.json()
      }
    }
  }
}

Note! When using Typescript you have to define your effects as functions (as shown above), not methods. This is common convention, though pointing out it being necessary for typing to work.

Typing

You got typing straight out of the box with immer-store.

const store = createStore({
  state: {
    title: ''
  },
  actions: {
    changeTitle: ({ state }, title: string) => {
      state.title = title
    }
  }
})

store.state.foo // string
store.actions.changeTitle // (title: string) => void

As you scale up you want to do this:

import { IAction } from 'immer-store'

import * as home from '../pages/home'
import * as issues from '../pages/issues'

const state = {
  home: home.state,
  issues: issues.state
}

const actions = {
  home: home.actions,
  issues: issues.actions
}

const effects = {}

export const config = {
  state,
  actions,
  effects
}

// This type can be used within the pages to define actions
export interface Action<Payload = void>
  extends IAction<Payload, typeof state, typeof effects> {}

And then in for example pages/home/

/*
  ./index.ts
*/
export { state } from './state'
export * as actions from './actions'
export const Home: React.FC = () => {}

/*
  ./state.ts
*/
type State {
  title: string
}

export const state: State = {
  title: ''
}

/*
  ./actions.ts
*/
import { Action } from '../store'

export const changeTitle: Action<string> = ({ state }, title) => {
  state.home.title = title
}

Target state

Since immer-store is tracking what components actually use it has a pretty nice optimization especially useful in lists. You can target a piece of state and then ensure that the component only renders again if that specific piece of state changes.

const Todo: React.FC<{ id: string }> = ({ id }) => {
  const todo = useState((state) => state.posts[id])
  const actions = useActions()

  return (
    <li>
      <h4>{todo.title}</h4>
      <input
        checked={todo.completed}
        onChange={() => actions.toggleTodo(id)}
      /> {todo.description}
    </li>
  )
}

Since we target the todo itself this component will only reconcile again if any of its accessed properties change, for example the completed state. If you were to rather pass the todo as a prop also the component passing the todo would need to reconcile on any change of the todo. This is an optimization that only makes sense in big lists where individual state items in the list change. Normally when you want to target state you can just:

const SomeComponent: React.FC_ = () => {
  const { home, issues } = useState()

  return (
    ...
  )
}

Computed

immer-store includes the reselect library for cached computed state. immer-store exposes a createComputed and useCOmputed hook that allows you to compute state:

const getTitle = (state) => state.title
const titleUpperCase = createComputed(
  [getTitle],
  (title) => title.toUpperCase()
)

const SomeComponent: React.FC_ = () => {
  const title = useComputed(titleUpperCase)

  return (
    ...
  )
}

The computed concept is just a tiny wrapper around reselect to manage updates.

Debugging

immer-store knows a lot about your application:

  • It knows what actions are changing what state
  • It knows what state all components are looking at
  • It knows which component renders related to what state change

It is possible to increase the insight even more to get a similar development tool experience as overmindjs.

What is missing?

  • Immer provides the concept of snapshots and patching. This is available, just not implemented. The question is what kind of API you want. An example would be to expose a default effect which allows you to start tracking changes to a specific state path with a limit of number of changes. Then you could takes these snapshots and patch them back in