medama
Version:
A minimalist, powerful, and dependency-free reactive state management library for TypeScript and JavaScript applications.
249 lines (183 loc) • 8.23 kB
Markdown
# _Medama_

_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**.
## Installation
```bash
npm i medama
```
## State
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_,
```ts
const { readState, subscribeToState, setState, resetState, pupil } = createMedama<State>();
```
and conveniently, the state can be set up during this creation phase.
```ts
const initState: Partial<State> = { foo: 'foo_record', bar: 'bar_record' }
const { readState, subscribeToState, setState, resetState, pupil } = createMedama<State>(initState);
```
## Selectors
Selectors serve as the primary interface for reading the state and subscribing to state changes.
```ts
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.
```ts
readState(({ foo }) => ({ foo })); // { foo: 'foo_record' }
```
## Subscriptions
Subscribing to state changes is achieved by pairing a selector with a subscription. In its simplest
form, a subscription manifests as a subscription job.
```ts
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.
```ts
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.
```ts
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'
```
### Subscription Methods
Each subscription returns methods to manage its lifecycle:
```ts
const { unsubscribe, resubscribe, transfer } = subscribeToState(selector, (value) => {
console.log(value);
});
```
- `unsubscribe`: Removes the subscription and cleans up associated resources
```ts
unsubscribe(); // Subscription stops receiving updates
```
- `resubscribe`: Updates the subscription with a new subscription function while maintaining the
same selector
```ts
resubscribe((value) => {
console.log('New subscription job:', value);
});
```
- `transfer`: Moves the subscription to a new selector while keeping the same subscription job
```ts
transfer(newSelector); // Same job runs with different selector's result
```
### Factory Functions and Subscription Methods
When using a factory function pattern for subscriptions, the initialization and subscription jobs
behave differently with `resubscribe` and `transfer`:
```ts
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.
## Setters
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.
```ts
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.
```ts
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.
```ts
subscribeToState(selector, (value) => {
console.log(value);
})
setState({ bar: 'very_new_bar' });
// print 'next_new_foo very_new_bar'
```
## Reset state
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.
```ts
resetState(initState);
```
It's important to note that resetting the state dissolves all existing subscriptions, providing a
truly clean slate.
## `pupil`
The `createMedama` function yields a `pupil` object, which encapsulates all available methods.
```ts
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:
```ts
// 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.
## Notes about performance optimization
_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.