It's a ridiculously tiny and highly performant state mangement solution when you don't want to implement redux. It's supposed to be minimalistic (comes in most handy for a source of state for library developers) and extremely simple. It comes with everything you need (including a super tiny memoized selector implementation) to maintain your state.
Install it as a dependency in your JavaScript/Typescript project
npm install @zuze/stateful
# or
yarn add @zuze/stateful
Or just pull it in from the browser:
<script src="https://unpkg.com/@zuze/stateful"></script>
<script>
const { state } = stateful;
const myState = state('jim!');
myState.subscribe(console.log); // jim!
</script>
state(initialState: T): Stateful<T>
Create a stateful instance with an initial state. Returns the stateful interface:
-
getState(): T
Returns the current state. -
setState((state: T) => T): void
Can be used set state using a functionimport { state } from '@zuze/stateful'; const s = state({ fetching: false, error: false }); s.setState(state => ({ ...state, fetching: false, data: 'some data' })); // { fetching: false, error: false, data: 'some data' }
import { state } from '@zuze/stateful'; const s = state({ fetching: false, error: false }); s.setState(state => ({ ...state, fetching: false, data: 'some data' })); // { fetching: false, error: false, data: 'some data' }
-
subscribe(subscriberFunction: Subscriber<T>): Unsubscribe
Register a subscriber function to be notified every time the state changes (see selectors). Returns an unsubscribe function.const s = state('jim'); const unsub = s.subscribe(console.log); // logs jim s.setState(() => 'fred'); // logs fred unsub(); s.setState(() => 'bill'); // nothing logged
createSelector(...selectors, combiner)
The purpose of a selector (popularized in reselect) is to minimize expensive computations through memoization.
There is an alternate method for using selectors outside of minimizing expensive computations: because the combiner function only gets called when at least one of it's arguments change, it essentially becomes a callback for changes in the input selectors.
import { createSelector, state } from '@zuze/stateful';
const myFetchingSelector = createSelector(
({ fetching }) => fetching,
(fetching) => {
console.log("fetching changed",fetching);
}
);
const s = state({
fetching: false,
data: null,
error: true;
});
s.subscribe(myFetchingSelector); // logs "fetching changed",false
(async() => {selectors
s.setState(state => ({ ...state, fetching: true })); // logs "fetching changed",true
try {
const data = await someAPICall();
s.setState(state => ({ ...state, data })) // not called!
} catch {
s.setState((state => ({ ...state, error: true })) // not called!
}
s.setState((state => ({ ...state, fetching: false })); // logs "fetching changed",false
});
Bare bones memoization implementation. You aren't allowed to mess with the comparator
import { memo } from '@zuze/stateful';
const myFunc = (...someArgs) => {
// ... some expensive computations
return 42;
};
const memoed = memo(myFunc);
console.log(memoed(...someArgs)); // outputs 42 - expensive computations performed
console.log(memoed(...someArgs)); // outputs 42 - expensive computations skipped!
There are 2 levels of memoizations going on when creating a selector.
- The function returned from
createSelector
itself is memoized usingchecker
- The
combiner
function is memoized usingchecker
.
What this effectively means is:
- If the function returns from
createSelector
is called with the same arguments neither the input selectors nor the combiner will be called - If the function returned from
createSelector
is called with different arguments, all input selectors will be called. If these executions result in the same arguments as the last time, thecombiner
will NOT be called.