Kex is a tiny library for state managenent in JavaScript/TypeScript projects.
- Small and fast
- Zero dependencies
- Async actions, cache, chainable actions out of box
- Written on Typescript
npm i kex
Just like Redux, Kex has reducers, actions and application state. But this time it's much simplier, because it not rescrict reducers to be pure functions (but they might be if you will).
Actions is objects that contain type
and payload
field:
{
type: string;
payload?: any;
}
You can dispatch action using dispatch
method:
import { createStore } from 'kex';
const store = createStore();
const dispatcher = store.dispatch(action);
// will print current state after action finishes
dispatcher.then(state => console.log);
Reducers appears to be iterators which can return modifiers or promises which should be resolved into modifiers indeed. Modifiers is objects containing information how you want to change the state.
Let's say we have the state:
const state = {
token: null,
credentials: {
login: 'example@mail.com',
password: '********'
}
}
and we want to change login
field to something else:
import { applyModifiers } from 'kex'; // apply modifiers to specific object
const changeLoginModifier = {
credentials: {
login: 'whatever@we-want.com'
}
}
// this will change state to:
// const state = {
// token: null,
// credentials: {
// login: 'whatever@we-want.com',
// password: '********'
// }
// }
applyModifiers(state, changeLoginModifier);
As you can see, modifiers may contain only changes we want to apply to our state rather than whole state. applyModifiers
works recursively and merge change of any level right into your state.
A typical reducer may look like this:
const function* reducer(action: KxAction) {
switch (action.type) {
...
case 'action_name':
yield { some: 'changes' };
break;
...
case 'another_action_name':
// async reducers out of box
yield Promise.resolve({ more: 'changes' });
break;
...
}
}
Also Kex allows you to create chainable actions. This means that your reducer may dispatch actions on its own. To do that you should use actions
field on the top of your state:
const function* reducerWithChainableAction(action: KxAction) {
switch (action.type) {
...
case 'chainable_action':
const { actions } = store.getState()
yield {
some: 'changes',
actions: [
// if other reducers added thier actions in queue
...actions,
// after current action perform specified actions
{ type: 'action_name' },
{ type: 'another_action_name', payload: your_payload }
]
};
break;
...
}
}
actions
field creates and clears automatically, all you need to do is add some actions in case you need it.
To set reducers use replaceReducers
method:
import { createStore } from 'kex';
const store = createStore();
store.replaceReducers(...reducers);
Reducers order in array actually matters because resolving process in linear. It means that reducers change the state one by one even if they async.
You can import storage at any point of your application. Use method getState
to get current state:
import { createStore } from 'kex';
const store = createStore();
store.getState();
To modify your state without actions you may use update
method:
import { createStore } from 'kex';
const store = createStore();
store.update(modifier);
You can subscribe to state changes using addStorageListener
(and cancel the subscription using removeStorageListener
method):
import { createStore } from 'kex';
const store = createStore();
const listener = (state, change) => {
console.log(change.action === null ? 'Changed manually' : `By ${change.action} action`);
console.log(`Changes: `, change.changes);
};
store.addStorageListener(listener);
store.removeStorageListener(listener);
You also can get history of state change using history()
method:
import { store } from 'kex';
store.setHistoryMaxSize(10); // 10 is default value
store.history(); // will return 10 last actions
To clear your state use clear
method:
import { store } from 'kex';
store.clear();
update
and clear
methods will appear in history, but action
field will be set to null
.
import { createStore, KxAction } from 'kex';
const store = createStore();
store.update({
counter: 0,
thisWillNotBeChanged: null
})
const function* counterReducer(action: KxAction) {
const { counter } = kxStore.getState();
switch (action.type) {
case 'INCREMENT':
yield {
counter: counter + 1;
};
break;
case 'ADD':
if (action.payload < counter) {
return;
}
yield {
actions: Array(action.payload - counter).fill({ type: 'INCREMENT' })
}
}
}
store.replaceReducers(counterReducer);
// {
// counter: 10,
// thisWillNotBeChanged: null
// }
store.dispatch({ type: 'ADD', payload: 10 }).then(console.log);
Kex also support cache feature (key-value storage inside your state). You can change cache manually using dispatch
and update
methods but we strongly recommend to use setCache
method insted. You can get your cache records using getCache
method:
import { createStore } from 'kex';
const store = createStore();
store.setCache('key', 'value'); // save key-value pair in cache (token === undefined)
// You can use tokens to invalidate cache after token expires or changes
store.setCache('foo', 'bar', 'token') // save key-value pair in cache (token === 'token')
/*
* After this state will look like this:
* {
* actions: [],
* cache: {
* key: {
* token: undefined,
* value: 'value'
* },
* foo: {
* token: 'token',
* value: 'bar'
* }
* }
* }
*
*/
store.getCache('wrong_key') // null
store.getCache('key'); // 'value'
store.getCache('foo'); // null
store.getCache('foo', 'token') // 'bar'
store.getCache('foo', 'wrong_token') // null
// You can change token and value any time
store.setCache('foo', 'baz', 'token');
store.getCache('foo', 'token'); // 'baz'
store.setCache('foo', 'new_baz', 'new_token');
store.getCache('foo', 'new_token'); // 'new_baz'
If you want to contribute to project please contact me. I'm open to discuss the concept.