Medama is a minimalist yet powerful universal reactive state management library crafted in TypeScript. It boasts high performance optimization and efficient garbage collection. At its core, medama operates on four fundamental concepts: state, selectors, subscriptions, and setters.
npm i medama
In medama, the state is represented by a streamlined flat object, with each key holding a distinct state record. The journey begins with initializing medama,
const { readState, subscribeToState, setState, resetState, pupil } = createMedama<State>();
and conveniently, the state can be set up during this creation phase.
const initState: Partial<State> = { foo: 'foo_record', bar: 'bar_record' }
const { readState, subscribeToState, setState, resetState, pupil } = createMedama<State>(initState);
Selectors serve as the primary interface for reading the state and subscribing to state changes.
const selector = ({ foo, bar }: State) => foo + bar;
readState(selector); // 'foo_record bar_record'
They offer a refined method to access individual records from the state, ensuring precise data retrieval.
readState(({ foo }) => ({ foo })); // { foo: 'foo_record' }
Subscribing to state changes is achieved by pairing a selector with a subscription. In its simplest form, a subscription manifests as a subscription job.
subscribeToState(selector, (value) => {
console.log(value);
});
// print 'foo_record bar_record'
By default, the subscription job activates immediately upon establishment. For cases where immediate execution isn't desired, the subscription can be defined as a function that returns the subscription job.
subscribeToState(selector, () => (value) => {
console.log(value);
});
// no side effect
medama also offers the flexibility to define separate jobs: one that runs at the moment of subscribing, and another that registers as an ongoing subscription job.
subscribeToState(selector, (value) => {
console.log('This is the first run with value:', value)
return (value) => {
console.log(value);
};
});
// print 'This is the first run with value: foo_record bar_record'
Each subscription returns methods to manage its lifecycle:
const { unsubscribe, resubscribe, transfer } = subscribeToState(selector, (value) => {
console.log(value);
});
-
unsubscribe
: Removes the subscription and cleans up associated resources
unsubscribe(); // Subscription stops receiving updates
-
resubscribe
: Updates the subscription with a new subscription function while maintaining the same selector
resubscribe((value) => {
console.log('New subscription job:', value);
});
-
transfer
: Moves the subscription to a new selector while keeping the same subscription job
transfer(newSelector); // Same job runs with different selector's result
When using a factory function pattern for subscriptions, the initialization and subscription jobs
behave differently with resubscribe
and transfer
:
const { resubscribe, transfer } = subscribeToState(selector, (value) => {
console.log('Init with:', value);
return (value) => {
console.log('Update:', value);
}
});
// Resubscribe acts like a new subscription but keeps the original selector,
// effectively unsubscribing from previous subscription first
resubscribe((value) => {
console.log('New init with:', value);
return (value) => {
console.log('New update:', value);
}
});
// With transfer, only the subscription part runs with new selector result
// The init part is never re-executed
transfer(newSelector); // Runs only 'Update: <new_value>'
The factory function pattern is useful when you need setup logic that runs only once during
subscription initialization, while transfer
allows changing what data is observed without
re-running this setup.
State updates in medama are handled through a smooth merging process of an object into the state object. The most straightforward approach is to directly pass the object for merging.
setState({ foo: 'new_foo' });
readState(({ foo, bar }) => ({ foo, bar })); // { foo: 'new_foo', bar: 'bar_record' }
Alternatively, a setter can be employed. Similar to a selector, a setter takes the state object as a parameter but returns an object destined for merging into the state.
setState(({ foo, bar }) => ({ foo: 'next_' + foo, bar: 'next_' + bar }));
readState(({ foo, bar }) => ({ foo, bar })); // { foo: 'next_new_foo', bar: 'next_bar_record' }
Any modifications to a single state record trigger the execution of subscription jobs that are subscribed to selectors depending on that record, ensuring targeted and efficient reactive updates.
subscribeToState(selector, (value) => {
console.log(value);
})
setState({ bar: 'very_new_bar' });
// print 'next_new_foo very_new_bar'
Occasionally, a complete state reset is necessary. The resetState
method caters to this need,
allowing for a fresh start. During this reset, the state can be initialized with new values if
desired.
resetState(initState);
It's important to note that resetting the state dissolves all existing subscriptions, providing a truly clean slate.
The createMedama
function yields a pupil
object, which encapsulates all available methods.
const { pupil } = createMedama();
const { readState, subscribeToState, setState, resetState } = pupil;
The return of createMedama
itself can be considered as the pupil object - the separate pupil
member in the returned object is provided for convenience when you need both the pupil object and
specific methods simultaneously. Here's a practical example using a derivative library:
// Using @medamajs/compose - a standalone derivative library that enables composition
// of multiple medama states into a cohesive unit
const { subscribeToState, setState, pupil } = createMedama();
subscribeToState(
(state) =>
//...
)
// Using @medamajs/compose - a standalone derivative library that enables composition
// of multiple medama states into a cohesive unit
const composed = composeMedama({
layer1: pupil,
layer2: createMedama(),
//...
});
The name "Medama" draws inspiration from the Japanese word 目玉, meaning "eyeball," with the pupil
representing the core of medama. This pupil
object is designed to facilitate the development of
supporting libraries. Moreover, the pupil
object can be passed as an opaque object within these
libraries, enabling the establishment of interconnected logic based on state updates.
Medama's design prioritizes efficiency. It recalculates selector values only when dependency records have been updated and active subscriptions exist for that selector. In scenarios with multiple subscriptions or the need for arbitrary state reading, the selector's calculated value is memoized following the most recent state update, avoiding unnecessary recalculations.
This optimization hinges on selector identity consistency. For the memoization to function correctly, the same selector instance must be used, rather than just identical function literals. This design choice influences how medama determines state record dependencies. Upon the first subscription establishment, the selector's value is calculated to identify its dependencies. For subsequent subscriptions, if the dependency records remain unchanged and the selector instance is consistent, medama skips recalculation, leveraging the existing results.
Importantly, medama provides an additional layer of safety. The state object passed to selectors and setters, if captured, does not allow direct access to the state. This access is restricted to within medama itself during the processing of selectors and setters, ensuring data integrity and preventing unauthorized state modifications.