jastaman
TypeScript icon, indicating that this package has built-in type declarations

0.0.25 • Public • Published

Jastaman

Just another state manager for React

Main goal is to create as concise library as possible

Inspired by Zustand and has similar API. Jastaman passes the same tests, has the same possibilities except middleware yet, performs faster.

Table of Contents

Basic usage

Defining a store looks similar to Zustand, with few differences:

  • state of the store is separated into state
  • to mutate store state use set function from store instance
  • current state of the store is accessible by store.state

In this way typescript can infer state type so it can be used in store methods, while in Zustand it is required to define type for the store explicitly.

State passed to the set function is partial object as well as in Zustand.

import { createStore } from 'jastaman'

export const store = createStore({
  state: {
    penguins: 0,
    lamas: 0,
    parrots: 0,
  },
  addPenguin: () => store.set(state => ({ penguins: state.penguins + 1 })),
  addLama: () => store.set({ penguins: 0 }),
  hasPenguins: () => store.state.penguins.length > 0
})

Use store in component:

import store from './store-from-above'

function Animals() {
  // get value just like in other state managers
  const penguins1 = store.use(state => state.penguins)
  
  // short syntax for one value (TypeScript knows the type)
  const penguins2 = store.use('penguins')
  
  // get multiple values at once (TypeScript is fine here as well)
  const { penguins, lamas, parrots } = store.use('penguins', 'lamas', 'parrots')
  
  return <h1>{penguins} penguins, {lamas} lamas and {parrots} parrots</h1>
}

function Controls() {
  // no need to use hooks to get store methods as we know they won't ever change
  return (
    <>
      <button onClick={store.addPenguin}>add penguin</button>
      <button onClick={store.addLama}>add lama</button>
    </>
  )
}

Api Overview

Create store

Simply create

Store can be created into a variable, it can be exported and imported across the project.

This is not recommended if you have plans for server rendering, because single store in this case will be used for different requests

import { createStore } from 'jastaman'

const store = createStore({
  state: {
    count: 123,
  },
  increment: () => store.set(state => ({ count: count + 1 }))
})

function Component() {
  const count = store.use('count')
  
  return (
    <div>
      count: {count}
      <button onClick={store.increment}>+</button>
    </div>
  )
}

TypeScript store type can be set explicitly, otherwise it will be inferred:

import { createStore } from 'jastaman'

type CounterStore = {
  state: {
    count: number
  },
  increment(): void
}

const store = createStore<CounterStore>({
  state: {
    count: 123,
  },
  increment: () => store.set(state => ({ count: count + 1 }))
})

useCreateStore hook

This hook can be used, for example, when you want to put multiple stores into one React Context.

In such case, top component will create multiple stores with hooks and put them into single context.

import { useCreateStore } from 'jastaman'

function Component() {
  const store = useCreateStore(() => ({
    state: {
      value: 123
    }
  }))
  
  const value = store.use('value')
  // ...
}

useCreateStore is a shortcut for useMemo, above example is the same as:

import { createStore } from 'jastaman'
import { useMemo } from 'react'

function Component() {
  const store = useMemo(() => {
    return createStore({
      state: {
        value: 123
      }
    })
  }, [])
  
  const value = store.use('value')
  // ...
}

useCreateStore optionally accepts option setOnChange

setOnChange can be used to set state from outside of the store to the state.

For example, here react-query result is saved to the store and keeps it updated.

import { useCreateStore } from 'jastaman'
import { useQuery } from 'react-query'

function Component() {
  const { data, isLoading, error } = useQuery('key', () =>
    fetch('...')
  )
  
  const store = useCreateStore(() => ({
    state: {
      data,
      isLoading,
      error,
    }
  }), {
    setOnChange: { data, isLoading, error }
  })
}

Placing store in React Context

import { createContext, createStore } from 'jastaman'

const { Provider: MyStoreProvider, useStore: useMyStore } = createContext(
  // callback is accepting inital data and returns the store
  ({ one, two }: { one: number; two: number }) =>
    createStore({
      state: {
        one,
        two,
      }
    })
)

const TopLevelComponent = () => {
  return (
    // Provider is accepting inital data as props
    <MyStoreProvider one={1} two={2}>
      <Component />
    </MyStoreProvider>
  )
}

const Component = () => {
  const store = useMyStore()
  const { one, two } = store.use('one', 'two')
  // ...
}

Computed (derived) fields

Computed fields are for the case when some state can be calculated from another state.

Especially it is useful when complex calculation is required, consuming CPU and time.

In this example imagine we have a data tree and we need to calculate statistics for it:

import { createStore, computed } from 'jastaman'

const store = createStore({
  state: {
    largeAndDeepDataTree: [{ ... }, { ... }],
    someProperty: 123,
    
    // define a computed field in this way
    // only for TypeScript
    // and only if store type is not provided explicitly
    statistics: computed<number>(),
  },
  computed: {
    statistics: [
      // first function is a selector - return array of fields which calculation depends on
      (state) => [state.largeAndDeepDataTree],
      // second function is to calculate the value
      (state) => calculateStatistics(state.largeAndDeepDataTree)
    ]
  },
})

const ShowStatistics = () => {
  const statistics = store.use('statistics')
  
  const updateData = (someData) => {
    // After setting data statistics will be recalculated
    // component will re-render with new statistics
    store.set({ largeAndDeepDataTree: someData })
  }
  
  const updateSomeProperty = (value) => {
    // This property is not selected by computed field, so this won't trigger recalculation
    // component will not re-render
    store.set({ someProperty: value })
  }
  
  // show statistics
}

Store instance API

store.use

When it is used with function it can accept up to 3 arguments:

store.use(selector, deps, equalityFn)

Outside variables should be listed in deps, example:

import { createStore } from 'jastaman'

const store = createStore({
  state: {
    items: {
      1: { title: 'one' },
      2: { title: 'two' },
    }
  }
})

const Component = ({ id }: { id: number }) => {
  // id is outside variable for selector and should be listed in array after selector
  const item = store.use(state => state.items[id], [id])
}

Third argument is to check if selected value was changed, by default it is Object.is.

Use it to return array or object from use:

import { shallowEqual } from 'jastaman'

const Component = ({ id }: { id: number }) => {
  // return array
  const [a, b, c] = store.use(state => [state.a, state.b, state.c], [], shallowEqual)
  
  // return object
  const { a, b, c } = store.use(state => ({ a: state.a, b: state.b, c: state.c }), [], shallowEqual)
}

You can pass state key or multiple keys instead of selector:

import { createStore } from 'jastaman'

const store = createStore({
  state: {
    a: 1,
    b: 2,
    c: 3,
  }
})

const Component = ({ id }: { id: number }) => {
  // single value
  const a = store.use('a')
  
  // multiple values
  const { a, b, c } = store.use('a', 'b', 'c')
}

store.state

Current state of the store

Variable reference remains the same after set or replace:

import { createStore } from 'jastaman'

const store = createStore({
  state: {
    a: 1
  }
})

const state = store.state
store.set({ a: 2 })

state.a === 2 // state was mutated
state === store.state // reference remains the same

store.prevState

When store is initialized prevState === state, and then it is updated before set or replace

store.set

Assign new data to the store state

const store = createStore({
  state: {
    a: 1,
    b: 2,
    data: [] as number[],
  }
})

// { a: 2 } gets merged into state
store.set({ a: 2 })

store.state.a === 2
store.state.b === 2

// accepts callback
store.set((state) => ({ data: [ ...state.data, 1 ] }))

// above is just the same as:
store.set(() => ({ data: [ ...store.state.data, 1 ] }))

store.replace

Replace state instead of merging, accepts object or callback like set

const store = createStore({
  state: {
    a: 1,
    b: 2 as number | undefined, // mark the field as optional
  }
})

store.replace({ a: 2 })
store.state.b === undefined // b is gone

store.subscribe

Subscribe to store updates

With 1 argument invokes listener on any change prevState === state if there were no updates yet

store.subscribe((state, prevState) => {})

With 2 arguments subscribe invokes listener only when selected values change

listener receives selected slice, not whole state, in this case single value

const selector = (state) => state.foo
store.subscribe(selector, (slice, prevSlice) => {})

With 3 arguments first is selector, second is equality function and third is listener

By default equality function is Object.is, which is almost identical to ===

import { shallowEqual } from 'jastaman'

store.subscribe(selector, shallowEqual, (slice, prevSlice) => {})

store.destroy

Remove all event listeners of the store

store.useEffect

It is a React hook, accepts the same arguments as store.subscribe + optional dependencies array

It can be used if you want to update state of one store based on state of another store

import { useCreateStore } from 'jastaman'
import { shallowEqual } from 'jastaman'

const Component = () => {
  const store = useCreateStore(() => ({
    state: {
      a: 1,
      b: 2,
      c: 3
    }
  }))
  
  store.useEffect(
    (state) => [state.a, state.b],
    shallowEqual,
    (slice, prevSlice) => {
      console.log('a or b changed!')
      console.log(`new values: [${slice.a}, ${slice.b}]`)
      console.log(`previous values: [${prevSlice.a}, ${prevSlice.b}]`)
    }
  )
}

Middleware

Not implemented yet

Performance

In Zustand it is recommended to memoize selectors with useCallback

const fruit = useStore(useCallback(state => state.fruits[id], [id]))

But in Jastaman useCallback around selector won't have any effect, still you need to pass array of dependencies if selector depends on outside variable:

const fruit = store.use(state => state.fruits[id], [id])

Without outside variables no need to provide array:

const fruit = store.use(state => state.fruit)

Benchmark results on my machine for js-framework-benchmark:

(source and results are not published yet)

Versions

Current Tags

  • Version
    Downloads (Last 7 Days)
    • Tag
  • 0.0.25
    0
    • latest

Version History

Package Sidebar

Install

npm i jastaman

Weekly Downloads

0

Version

0.0.25

License

MIT

Unpacked Size

935 kB

Total Files

86

Last publish

Collaborators

  • romikus