A Pub/Sub state manager you'll love... or hate
npm install vegemite
* Familiar
_Inspired by Immer and Redux_
* Composable
_Attach/Detach action handlers as needed. Components can bring their own handlers!_
* Immutability
Modifications or mutations within a handler do not affect state until completion
* Full TypeScript Support
Utilize TypeScript for in-action guards and validations
* Extremely lightweight
Weighs only 623 bytes, including dependencies!
```
$ npm install --save vegemite
> Check out the /examples directory for more patterns.
`js
import vegemite from 'vegemite';
const todomvc = vegemite({
next: 1,
todos: [],
});
// handler-type: mutate
todomvc.on('todo:add', (state, data) => {
let nxt = state.next++;
state.todos.push({
id: nxt,
done: false,
title: data.title,
});
});
// handler-type: return object
todomvc.on('todo:toggle', (state, id) => {
let todos = state.todos.map(item => {
if (item.id === id) item.done = !item.done;
return item;
});
return { next: state.next, todos };
});
// handler-type: return Promise
todomvc.on('todo:assign', async (state, data) => {
await httpie.send('POST', /api/todos/${data.id}/assign, {
body: { user: data.userid }
});
state.todos = state.todos.map(item => {
if (item.id === data.id) {
item.assigned = item.assigned || [];
item.assigned.push(data.userid);
}
return item;
});
});
// Add a global "listener" (side effect)
// This runs _after_ state has been updated
todomvc.listen((state, prev) => {
console.log('~> this is the NEW state:', state);
console.log('~> this was the OLD state:', prev);
});
`
Returns a Vegemite instance, pre-filled with an initial state object.
#### state
Type:
State extends anyAny initial data of your choosing.
$3
Returns: StateA getter function that returns a snapshot of the current state.
$3
Returns: FunctionAssign a new action handler for a specific event.
Returns an unsubscriber function that, when called, will detach this
handler.> Important: When more than one
handler exists for an event, they are executed in the order that they were assigned.`js
let store = vegemite({ count: 5 });let detach = store.on('count:add', (state, event) => {
state.count += event.value;
});
await store.dispatch('count:add', { value: 10 });
console.log(store.state); //=> { count: 15 }
detach(); // deactivated
await store.dispatch('count:add', { value: 3 });
console.log(store.state); //=> { count: 15 } (unchanged)
`#### event
Type:
StringThe event/topic name.
#### handler
Type:
HandlerSee Handlers below.
$3
Returns: FunctionAttach a new subscription — when no
event is specified, then the subscriber will listen to _all_ events.Returns an unsubscriber function that, when called, will detach this
subscriber.> Important: When more than one
subscriber exists for an event (or globally), they are executed in the order that they were assigned.`js
let store = vegemite({ count: 5 });store.on('increment', state => {
state.count++;
});
let unlisten = store.listen('increment', (state, prevState) => {
console.log(
heard increment: ${prevState.count} ~> ${state.count});
});await store.dispatch('increment');
//=> heard increment: 5 ~> 6
unlisten(); // deactivated
await store.dispatch('increment');
// (silence)
`#### event
Type:
StringThe event/topic name, if any.
#### subscriber
Type:
SubscriberSee Subscribers below.
$3
Returns: voidForcibly set the instance's state.
When
set() is run, all related subscribers/listeners are called.
Optionally pass an event to trigger a specific set of listeners.dispatch() chain.#### state
Type:
StateThe new
State data.#### event
Type:
StringThe event/topic on whose behalf you're committing.
$3
Returns: PromiseSends a new message to an event/topic.
When
dispatch() is called, all related action handlers (via on()) are run to produce a new state. Once this chain is complete, set() is invoked with the new state, which will trigger any listeners for the event (in addition to global listeners).> Attention TypeScript Users:
For events that do not require
eventData, you must dispatch with an undefined payload, or similar.#### event
Type:
StringThe event/topic to target.
#### eventData
Type:
anyAny data to send the topic.
> Important: Your
eventData is sent to your event/topic's handlers as is!
Handlers
Action handlers can be thought of as "producers" or "reducers" – they are functions that, when matched, are invoked to help create a new state.
Action handlers belong to a specific event and are assigned via
on(). Multiple handlers may exist for a single event topic. When this is the case, the list of handlers will be executed in the order that they were assigned. This allows handlers to access previous handlers' results and use those as part of its own computation, if desired.Every handler receives the current
state as its first parameter and then any eventData that was sent to the event (via dispatch()). Handlers may return a new state object or they may choose to mutate the state in place and forgo a return entirely. A Promise may also be returned that resolves either to a new state or to a mutation.`ts
type Handler = (state: T, eventData: X) => Promise | T | void;
`While an event's handlers are running (dubbed the "dispatch chain"), you are free to mutate the
state and eventData object _as much as you'd like_ — mutations have no effect on the instance's actual state during this time! Once the "dispatch chain" has resolved (aka, _all_ handlers have completed), Vegemite will promote the chain's final state object as the instance's new actual state (via set(state, event)).At this point, the instance's state has been updated, which means
ctx.state will reflect the current changes. Additionally, any subscribers (via listen()) are enqueued for action.Subscribers
Subscribers, or listeners, run _after_ the instance has updated its internal state. These can be thought of as "callbacks" or side-effects that, when matched, should be alerted about a change in state.
All subscribers receive the current
state as its first parameter and the _previous_ state as its second parameter. You can use this information to infer what, specifically, changed between the two state objects. The return value from subscribers has no effect anywhere.`ts
type Listener = (state: T, prevState: T) => any;
`Subscribers can listen in on a specific event topic, or they can listen to everything (aka, "global" listeners).
When an event's "dispatch chain" has resolved, any subscribers for that event will be queued in the order that they were assigned. Additionally, all "global" subscribers, if any, will be executed in the order that they were defined. However, please note that global subscribers always execute before event-specific subscribers.
TypeScript Support
Vegemite is made to work with an "EventMap" — this is an interface whose keys are the names of your events and whose values are the event-datas you expect to pass along as the message. With this information alone, TypeScript and
vegemite can ensure that all of your event publishers and subscribers are passing or expecting the correct data types.Additionally,
vegemite requires a State descriptor — this is a separate interface that describes _what_ you want this vegemite instance to contain at all times. With this information, TypeScript can ensure your actions/handlers are abiding by the rules & not causing mischief.`ts
interface Todo {
id: number;
done: boolean;
title: string;
assigned?: string[];
}interface Assignment {
id: Todo['id'];
userid: string;
}
interface EventMap {
'todo:add': Todo['title'];
'todo:toggle': Todo['id'];
'todo:assign': Assignment;
// ...
}
interface State {
next: number;
todos: Todo[];
}
const todomvc = vegemite({
next: 1,
todos: []
});
`With the setup above, TypeScript can help us ensure that the
todo:add and todo:toggle topics will only send or receive their respective Todo attributes, whereas the todo:assign topic will always send or receive a more complex Assignment object.Meanwhile, any manipulations within the
State will be protected, ensuring that state.todos only contains valid Todo objects and state.next` is always a number.MIT © Luke Edwards