Very simple and powerful state management solution for any application built on Laco and Immer.
Set up your stores and subscribe to them. Easy as that!
npm install lacer
- 🚀 Simple to use
- 🎉 Lightweight (<7kbs gzipped)
- ✨ Partial Redux DevTools Extension support (time travel thanks to Laco)
import { Store } from 'lacer'
// Creating a new store with an initial state { count: 0 }
const CounterStore = new Store({ count: 0 })
// Implementing some actions to update the store
const increment = () => CounterStore.set((state) => state.count++)
const decrement = () => CounterStore.set((state) => state.count--)
increment()
expect(CounterStore.get().count).toBe(1) // Success
decrement()
expect(CounterStore.get().count).toBe(0) // Success
Check out Redux DevTools Extension.
Just click on the stopwatch icon and you will get a slider which you can play with. That's it! :)
Check out React Native Debugger.
Works as you would expect :)!
// Initializing a new store with an initial state and a name:
interface INewStore = {
count: number
}
const NewStore = Store<INewStore>({ count: 0 }, "Counter")
The name is optional and is used to get an overview of action and store relationship in Redux DevTools Extension. Action names for the Store will now show up as Counter - ${actionType}
in DevTools Extension where as before only ${actionType}
was shown.
// Getting the state of the store
Store.get()
Returns an object which could be something like { count: 0 }
following the example.
type SetStateFunc<T> = (state: Draft<T> => void)
Technically, this function does allow any value to be returned to allow patterns similar to what you'll see in the example where the assignment is returned. However, the return value is ignored.
T inherits from the generic specified during the initalization of the Store. The SetStateFunc provides a Draft<T>
from Immer. For more information on how to use Immer, check it out here.
// Setting a new state and passing an optional action name "increment"
Store.set((state) => (state.count++), "increment")
type ReplaceStateFunc<T> = (state: T) => T
T inherits from the generic specified during the initalization of the Store. This function allows you to completely replace the object used in the store and fires all subscription handlers afterwards regardless of the properties they listen to.
// Setting a new state and passing an optional action name "increment"
Store.replace((state) => { /* return modified state */}, "increment")
type MiddlewareFunc<T> = (state: T, draft: Draft<T>, actionType?: string) => boolean | undefined
A middleware will intercept a state change (set or replace) before it is made. If the middleware returns false, the change is discarded. If it returns true or undefined, the next middleware will run. Setting values is identical to Store.set
since it uses immer internally as well. For setting the state, use draft
. For getting the state, use state
.
Note: You can mutate this object freely since it is generated by Immer.
// Setting a condition to prevent count from going below 0
// and a special case for `SudoDecrement` action which CAN make count go below 0
CounterStore.addMiddleware((state, _, actiontype) => state.count >= 0 || actionType === 'SudoDecrement')
type MiddlewareFunc<T> = (state: T, draft: Draft<T>, actionType?: string) => boolean | undefined
The remove middleware function takes a middleware that matches a previously added middleware and removes it.
// Setting a condition to prevent count from going below 0
// and a special case for `SudoDecrement` action which CAN make count go below 0
const myMiddleware = (state) => (state.count++)
CounterStore.addMiddleware(myMiddleware)
CounterStore.set((state) => (state.count = 0))
expect(CounterStore.get().count).toBe(1) // Success
CounterStore.removeMiddleware(myMiddleware)
CounterStore.set((state) => (state.count = 0))
expect(CounterStore.get().count).toBe(0) // Success
type ListenerFunc<T> = (state: T, oldState: T, changes?: string[]) => void
A subscriber will fire whenever its properties match the properties changed during a set. It will always fire when a replace or reset is called. If properties is undefined, it will always fire on state change.
Note: DO NOT attempt to mutate the state object as it will throw an error.
// Setting a condition to prevent count from going below 0
// and a special case for `SudoDecrement` action which CAN make count go below 0
const unsubscribe = CounterStore.subscribe(
(state, oldState) => console.log(state.count, state.oldCount),
['count']
)
CounterStore.set((state) => (state.count++)) // "1 0" printed to console
CounterStore.set((state) => (state.someOtherProperty++)) // Nothing printed to console
unsubscribe()
CounterStore.set((state) => (state.count++)) // Nothing printed to console
type ListenerFunc<T> = (state: T, oldState: T, changes?: string[]) => void
The unsubscribe function takes a listener function that matches a previously added subscription and removes it.
// Setting a condition to prevent count from going below 0
// and a special case for `SudoDecrement` action which CAN make count go below 0
const myListener = (state) => (console.log(state.count))
CounterStore.subscribe(myListener)
CounterStore.set((state) => (state.count++)) // "1" printed to console
CounterStore.unsubscribe(myListener)
CounterStore.set((state) => (state.count++)) // Nothing printed to console
// Resets the store to initial state
Store.reset()
Reset will bring back the original state of the Store when it was initialized. This is stored in Store.initialState
. It will return true on success and false on failure. If force is true, it will run all middleware regardless of their success and will always reset the store.
// Dispatching an action that does not change the state of the store
Store.dispatch(changeLocation(), "Location change")
You might want to dispatch an action that is associated with a certain store but don't want to change the state. The action will in this case be shown as StoreName - Location change
.
import { dispatch } from 'lacer'
// Dispatching a global action that does not change any state
dispatch(changeLocation(), "Location change")
You might want to dispatch a global action that is NOT associated with any store. The action will in this case just be shown as Location change
.
Testing using jest:
import { ICounterState } from '/types'
import { Store } from 'lacer'
test('CounterStore simple actions', () => {
// Creating a new store with an initial state { count: 0 }
const CounterStore = new Store<ICounterState>({ count: 0 }, 'Counter')
// Implementing an action to update the store
const increment = () => CounterStore.set((prev) => prev.count++, 'Increment')
expect(CounterStore.get().count).toBe(0)
increment()
expect(CounterStore.get().count).toBe(1)
})
Based on:
Depends on:
Special Thanks
- Austin McCalley for the name