0.8.1 • Published 1 year ago

@gapu/deepstate v0.8.1

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

@gapu/deepstate

A small, fast and no-boilerplate state-management library for react, using hooks.

npm bundle size MIT License npm (scoped) GitHub Repo stars

Navigation

  1. Installation
  2. Description
  3. Usage
  4. Plugins
  5. Planned Features

Installation ⚡

Warning
Deepstate only supports react version >= 16.8.0

npm install @gapu/deepstate

Description

Deepstate is a state-management library which is aimed at making nested state managemenet easier.

Note
This library is still in development

Version 1.0.0 will be released only after the planned features are implemented and the remaining bugs are fixed.

Collaborators are welcome ❤

If you come across an issue please report it on our github issues page: https://github.com/pureliani/deepstate/issues

Usage

Note
Context providers are not needed.

Primitive store

import { create } from '@gapu/deepstate'

const { useSelector } = create(11)

export const Counter = () => {

    // useSelector usually takes a selector function as an argument but
    // in this case we don't really need it since we are working with
    // a primitive store, not passing a selector will select the root of the
    // store by default, in this case: 11
    const [value, setValue] = useSelector()

    return (
        <div>
            <h3>Count: {value}</h3>
            <button onClick={() => setValue(current => current + 1)}>+ 1</button>
            <button onClick={() => setValue(current => current - 1)}>- 1</button>
        </div>
    )
}

Complex store

When working with a nested state, unlike other state management libraries, you are only concerned with the field that you are accessing.

import { create } from '@gapu/deepstate'

const { useSelector } = create({
    a: {
        b: 42,
        c: 1
    },
    d: 2
})

// useSelector takes a selector function as an argument which will subscribe the
// component to changes on that specific field. Now, the "Counter" component
// will only rerender when the state.a.b changes. Changing state.a.c or state.d
// will not trigger a rerender of this component.
export const Counter = () => {
    const [value, setValue] = useSelector(state => state.a.b)
    return (
        <div>
            <h3>Count {value}</h3>
            <button onClick={() => setValue(current => current + 1)}>+ 1</button>
            <button onClick={() => setValue(current => current - 1)}>- 1</button>
        </div>
    )
}

Asynchronous actions

import { create } from '@gapu/deepstate'

// Mock API call
const getRandomRemoteNumber = (): Promise<number> => {
  return new Promise((res) => {
    setTimeout(() => {
      res(Math.floor(Math.random()*1000))
    },  1 * 1000)
  })
} 

const { useSelector } = create({ count: 1 })

function App() {
  const [count, setCount] = useSelector(state => state.count)

  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => setCount(async () => {
        // if an error gets thrown or a promise gets rejected, state will not
        // get updated.
        const newNumber = await getRandomRemoteNumber()

        return newNumber
      })}>
        get random remote number
      </button>
    </div>
  )
}

export default App

Getting the state from outside of react

import { create } from '@gapu/deepstate'

const { getState, useSelector } = create({ count: 1 })

// you can get the current state of your store 
// by calling the 'getState' function.
const currentState = getState()

console.log(currentState) // { count: 1 }

export const Counter = () => {
    const [value, setValue] = useSelector(state => state.count)
    return (
        <div>
            <h3>Count {value}</h3>
            <button onClick={() => setValue(current => current + 1)}>+ 1</button>
            <button onClick={() => setValue(current => current - 1)}>- 1</button>
        </div>
    )
}

Setting the state from outside of react

import { create } from '@gapu/deepstate'

const { setState, getState, useSelector } = create({ count: 1 })

// you can also set the current state of your store 
// by calling the 'setState' function.
setState({ count: 42 })
console.log(getState()) // { count: 42 }

export const Counter = () => {
    const [value, setValue] = useSelector(state => state.count)
    return (
        <div>
            <h3>Count {value}</h3>
            <button onClick={() => setValue(current => current + 1)}>+ 1</button>
            <button onClick={() => setValue(current => current - 1)}>- 1</button>
        </div>
    )
}

Subscribing to changes

import { create } from '@gapu/deepstate'

const { useSelector, subscribe } = create({ count: 1 })

// Subscribe function takes a callback as an argument which will be executed 
// whenever any state change happens. it also returns an 'unsubscribe' function
// which you can call if you wish to stop listening to changes.
// The callback will receive the updated state as an argument.
const unsubscribe = subscribe((newState) => {
    console.log(newState)
})

export const Counter = () => {
    const [value, setValue] = useSelector(state => state.count)
    return (
        <div>
            <h3>Count {value}</h3>
            <button onClick={() => setValue(current => current + 1)}>+ 1</button>
            <button onClick={() => setValue(current => current - 1)}>- 1</button>
        </div>
    )
}

Server side state initialization ( Next.js )

import { create } from '@gapu/deepstate'
import { InferGetServerSidePropsType } from 'next'

const { initServerState, useSelector } = create({ a: { b: 1 } })

export const getServerSideProps = () => {
  return {
    props: {
      // ** NOTE ** this must have the same structure as the store
      initialState: {
        a: {
          b: 88
        }
      }
    }
  }
}

export default function Home({ initialState }: InferGetServerSidePropsType<typeof getServerSideProps>) {
  // ** IMPORTANT ** call this function before any 'useSelector'
  initServerState(initialState)

  const [count, setCount] = useSelector(state => state.a.b)
  return (
    <div>
      <h3>Count is {count}</h3>
      <button onClick={() => setCount(current => current + 1)}>+ 1</button>
      <button onClick={() => setCount(current => current - 1)}>- 1</button>
    </div>
  )
}

Plugins

Currently we have only two plugins: persist and broadcast.

Persisting state with the 'persist' plugin

import { create, persist } from '@gapu/deepstate'

// This state will be persisted in localStorage on the key 'localStorageKeyName'
// before using this middleware make sure the data is serializable 
// (is a valid JSON), otherwise data might be lost during internal 
// stringification.
const { useSelector } = create({
    a: 1,
    b: {
        c: 2
    }
}, [persist('localStorageKeyName')])

export const Counter = () => {
    const [value, setValue] = useSelector((state) => state.a)
    return (
        <div>
            <h3>Count: {value}</h3>
            <button onClick={() => setValue(current => current + 1)}>+ 1</button>
            <button onClick={() => setValue(current => current - 1)}>- 1</button>
        </div>
    )
}

Sharing state between browser tabs with the 'broadcast' plugin

import { create, broadcast } from '@gapu/deepstate'

// This state will be shared between browser tabs via BroadcastChannels on the
// 'broadcastChannelName' channel. please make sure your state can be cloned 
// via the 'structuredClone' function. otherwise data might be lost /
// application might crash.
const { useSelector } = create({
    a: 1,
    b: {
        c: 2
    }
}, [broadcast('broadcastChannelName')])

export const Counter = () => {
    const [value, setValue] = useSelector((state) => state.a)
    return (
        <div>
            <h3>Count: {value}</h3>
            <button onClick={() => setValue(current => current + 1)}>+ 1</button>
            <button onClick={() => setValue(current => current - 1)}>- 1</button>
        </div>
    )
}

Using both persist and broadcast in conjunction

import { create, persist, broadcast } from '@gapu/deepstate'

// This state will be shared between browser tabs via BroadcastChannels on the
// 'broadcastChannelName' channel, data will also persisted in localStorage on
// the key 'localStorageKeyName', please make sure your data can be
// deeply cloned via the 'structuredClone' and is also serializable
// (is a valid JSON)
const { useSelector } = create({
    a: 1,
    b: {
        c: 2
    }
}, [persist('localStorageKeyName'), broadcast('broadcastChannelName')])

export const Counter = () => {
    const [value, setValue] = useSelector((state) => state.a)
    return (
        <div>
            <h3>Count: {value}</h3>
            <button onClick={() => setValue(current => current + 1)}>+ 1</button>
            <button onClick={() => setValue(current => current - 1)}>- 1</button>
        </div>
    )
}

Custom plugins

import { create, Plugin } from '@gapu/deepstate'

// 'myLoggerPlugin' function will be invoked ONLY ONCE and 
// as soon as the 'storeAPI' gets created.
// you will need to pass a name to this plugin even if you dont need it.
const myLoggerPlugin: Plugin = (name) => (storeAPI) => {
  const {
    //Get the root state of the store
    getState, 
    
    // Set the root state of the store
    // This will not cause a rerender, if you want to do that, call the
    // 'notify' function with 'internal' as an argument right after setting the
    // state, e.g: notify(['internal'])
    setState,

    // notify(['internal']) - rerender components which are using the 
    // 'useSelector' hook, (rerender will only happen if the returned value from 
    // selector is different from the previous render as compared by Object.is )
    
    // notify(['channel']) - trigger the channel subscribers ( used by the
    // broadcast plugin ). 

    // notify(['external']) - trigger subscribers
    // e.g: subscribe((newState) => {console.log(newState)})
    notify,
    
    // This is used by react itself so we don't really need to touch this
    subscribeInternal,

    // Same as the 'subscribe' method returned by the 'create' function
    subscribeExternal,
    
    // Used by the 'broadcast' plugin, most likely you don't need this
    subscribeChannel
  } = storeAPI


  subscribeExternal((state) => {
      console.log(state)
  })
} 

const { useSelector } = create({ count: 1 }, [myLoggerPlugin('my-plugin')])


function App() {
  const [count, setCount] = useSelector(state => state.count)
  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => setCount(current => current + 1)}>
        + 1
      </button>
      <button onClick={() => setCount(current => current - 1)}>
        - 1
      </button>
    </div>
  )
}

export default App

Planned features

  1. Callback state initialization
0.8.1

1 year ago

0.8.0

1 year ago

0.7.1

1 year ago

0.7.0

1 year ago

0.6.2

1 year ago

0.6.1

1 year ago

0.6.0

1 year ago

0.5.2

1 year ago

0.5.1

1 year ago

0.4.2

1 year ago

0.4.1

1 year ago

0.4.0

1 year ago