@jackcom/raphsducks
v3.0.1
Published
A lightweight pubsub (publish-subscribe) state manager.
Downloads
18
Maintainers
Readme
Raph's Ducks v3
UPDATES:
- Version
1.X.X
simplifies the library and introduces breaking changes. If you're looking for the0.X.X
documentation (I am so sorry), look here,- Version
1.1.X
addstypescript
support, and a newsubscribeOnce
function (see below)- Version
2.X.X
introducesrxjs
under the hood- Version
3.X.X
replacesrxjs
withimmutablejs
for maximum profit
- Raph's Ducks v3
What is it?
- A simple Javascript state manager.
- API is based on the Redux core
- Subscribe to state with
subscribe
(returns an unsubscription function) - Get a copy of current state with
getState
- NO REDUCERS! Just update the key(s) you want with the data you expect.
- Subscribe to state with
- Can be used in a NodeJS backend, or with any UI library (React, Vue, Svelte, etc)
If it isn't the simplest state-manager you have ever encountered, I'll ...
I'll eat my very ~~javascript~~ typescript.
Installation
npm i -s @jackcom/raphsducks
Usage Overview
This library can be used alone, or in combination with other state managers. Here's what you do:
- Define a state, and
- Use it.
Defining your state
raphsducks
allows you to intuitively define your state once, in a single place. The libary turns your state representation into an object that you can observe or update in different ways.
The library exports a single function, createState
.
When called, this returns an State
instance, which
- Turns every state property into a setter function, and
- Provides additional functions for reading or subscribing to that state
/* MyApplicationStore.js */
import createState from '@jackcom/raphsducks';
// State definition: the object-literal you supply is your initial state.
const initialState = {
todos: [],
someOtherValue: false,
someCounter: 0,
someString: ''
}
// The state instance you will actual use. Instantiate, and you're ready to go.
const store = createState(initialState);
// (OPTIONAL) export for use in other parts of your app
export default store;
Hint: In typescript, a key initialized with
null
will always expectnull
as an update value. To prevent type assertion errors, make sure you initialize your keys with a corresponding type. (e.g.{ p: [] as string[] }
)
In the example above, both todos
and someOtherValue
will become functions on store
. See usage here
Working with Typescript
When working with TS, you'll want to cast object types in your initial state to avoid type assertion errors. This prevents array types from being initialized as never[]
, and lets the instance know what keys to expect from any child objects.
i. inline type definitions (recommended)
// A single `To do` object (e.g. for a to-do list) type ToDo = { title: string, description?: string, done: boolean }; // Initial state with inline type definitions const initialState = { todos: [] as ToDo[], // require an array of `ToDo` objects someOtherValue: false, // boolean (inferred) someCounter: 0, // number (inferred) someString: '' as string | null // will allow `null` for this key } const myStateInstance = createState(initialState); // update select keys myStateInstance.multiple({ someOtherValue: true, someCounter: 3, }); // Check results myStateInstance.getState().someOtherValue; // true myStateInstance.getState().someCounter; // 3
ii. Initial State Type Definitions
Note: This requires you to update your state type definition as well as your initial state object.
You can optionally create a type-def for the entire state, though this gets unwieldy to maintain. Inline definitions are cleaner and recommended (see above).
// IMPORTANT: DO NOT specify optional properties on the top level, or you'll // never hear the end of it. type MyStateTypeDef = { todos: ToDo[]; someOtherValue: boolean; someCounter: number; someString: stringl }; // A single `To do` object (e.g. for a to-do list) type ToDo = { title: string, value: boolean }; // USAGE 1: Type-cast your initial state to get TS warnings for missing properties. const initialState: MyStateTypeDef = { ... }; const myStateInstance = createState(initialState); // USAGE 2: Cast the `createState` function to get TS warnings here. const myStateInstance = createState<MyStateTypeDef>( /* initialState */ );
Updating your state instance
You can update one key at a time, or several at once. In Typescript, the value type is expected to be the same as the initial value type in state. Other types can usually be inferred.
// Updating one key at a time
store.todos([{ title: "Write code", value: true }]);
// Updating several keys. Subscribers are notified once per 'multiple' call.
store.multiple({
todos: [{ title: "Write code", value: true }],
someOtherValue: true,
});
Note that state.multiple( args )
will merge args
into the current state instance. Make sure you update object properties carefully (e.g. merge Array
properties before supplying them in args
)
// Updating an array property
const oldTodos = store.getState().todos
store.multiple({
todos: [...oldTodos, { title: "New task", value: false }],
someOtherValue: true,
});
Subscribing to your state instance
You can subscribe for updates and receive an unsubscribe
function. Call it when you no longer need to listen for updates. Your subscriber should take two values: the updated state
values, and a list of just-updated state property names.
const unsubscribe = store.subscribe((state, updatedKeys) => {
let myTodos;
// Handy way to check if a value you care about was updated.
if (updatedKeys.includes("todos")) {
myTodos = state.todos
}
});
// stop listening to state updates
unsubscribe();
state.subscribe()
is a great way to listen to every change that happens to your state. However, you will typically have to check the updated object to see if it has any values you want.
Luckily there are other ways to subscribe to your state instance. These alternatives only notify when something you care about gets updated. Some of them allow you to even specify what values you want to return.
Disposable (one-time) subscription
Use subscribeOnce
to listen to your state until a single value is updated (or just until the next state update happens), then auto-unsubscribe.\
Hint: the
listener
handler is the same in allsubscribe
functions. It always accepts two arguments: the updatedstate
object-literal, and a list of keys that were just updated.
Ad-hoc, one-time subscription
You can wait for the next state update to trigger something else.
const unsubscribe = store.subscribeOnce(() => {
doSomethingElse();
});
// Cancel the trigger by unsubscribing:
unsubscribe(); // 'doSomethingElse' won't get called.
One-time subscription to a specific value
Listen until a specific item gets updated, then use it. The value is guaranteed to be on the updated state object.
We'll use state.todos
in our example.
const unsubscribe = store.subscribeOnce((state) => {
const todos = state.todos;
doSomethingElse(todos);
}, 'todos');
// Cancel the state-update trigger by unsubscribing:
unsubscribe(); // 'doSomethingElse' won't get called.
Reference
createState
- Default Library export. Creates a new
state
instance using the supplied initial state. Parameters:- Args: An object-literal representing every key and initial/default value for your global state.
- API:
createState(state: { [x:string]: any }): ApplicationStore
- Returns: a state instance.
State Instance
An instance of ApplicationStore
with full subscription capability. This is distinct from your state representation.
State Representation
A plain JS object literal that you pass into createState
.
This object, for all intents and purposes, is your state. It should hold any properties you want to track.
You can modify/use your state representation via the State Instance
.
ApplicationStore (Class)
- State instance returned from
createState()
. View full API and method explanations here.class ApplicationStore { getState(): ApplicationState; multiple(changes: Partial<ApplicationState>): void; reset(clearSubscribers?: boolean): void; subscribe(listener: ListenerFn): Unsubscriber; subscribeOnce<K extends keyof ApplicationState>( listener: ListenerFn, key?: K, valueCheck?: (some: ApplicationState[K]) => boolean ): void; subscribeToKeys<K extends keyof ApplicationState>( listener: ListenerFn, keys: K[], valueCheck?: (key: K, expectedValue: any) => boolean ): Unsubscriber; // This represents any key in the object passed into 'createState' [x: string]: StoreUpdaterFn | any; }
Listener Functions
A listener
is a function that reacts to state updates. It expects one or two arguments:
state: { [x:string]: any }
: the updatedstate
object.updatedItems: string[]
: a list of keys (state
object properties) that were just updated.
Example Listener
A basic Listener receives the updated application state, and the names of any changed properties, as below:
function myListener(updatedState: object, updatedItems: string[]) {
// You can check if your property changed
if (updatedState.todos === myLocalStateCopy.todos) return;
// or just check if it was one of the recently-updated keys
if (!updatedItems.includes("todos")) return;
// `state.someProperty` changed: do something with it! Be somebody!
this.todos = updatedState.todos;
};
You can define your listener
where it makes the most sense (i.e. as either a standalone function or a method on a UI component)
What does it NOT do?
This is a purely in-memory state manager: it does NOT
- Serialize data and/or interact with other storage mechanisms (e.g.
localStorage
orsessionStorage
). - Prevent you from implementing any additional storage mechanisms
- Conflict with any other state managers
Deprecated Versions
Looking for something? Some items may be in v.0.5.x
documentation, if you can't find them here. Please note that any version below 1.X.X
is very extremely unsupported, and may elicit sympathetic looks and "tsk" noises.
Migrating from v1x
to v2x
Although not exactly "deprecated", v1.X.X
will receive reduced support as of June 2022. It is recommended that you upgrade to the v2.X.X
libraryas soon as possible. The migration should be as simple as running npm i @jackcom/raphsducks@latest
, since the underlying API has not changed.
iFAQs (Infrequently Asked Questions)
What is raphsducks
?
A publish/subscribe state-management system: originally inspired by Redux, but hyper-simplified.
Raphsducks is a very lightweight library that mainly allows you to instantiate a global state and subscribe to changes made to it, or subsets of it.
You can think of it as a light cross between Redux and PubSub. Or imagine those two libraries got into a fight in a cloning factory, and some of their DNA got mixed in one of those vats of mystery goo that clones things.
How is it similar to Redux?
- You can define a unique, shareable, subscribable Application State
- Uses a
createState
function helper for instantiating the state - Uses
getState
, andsubscribe
methods (for getting a copy of current state, and listening to updates).subscribe
even returns an unsubscribe function!
How is it different from Redux?
- No
Actions
. - No
dispatchers
- No
reducers
- No serialization
- You can use with (or without) any UI framework like ReactJS, SvelteJS, or Vue
1. Why did you choose that name?
I didn't. But I like it.
2. Does this need React or Redux?
Nope
This is a UI-agnostic library, hatched when I was learning React and (patterns from) Redux. The first implementation came directly from (redux creator) Dan Abramov's egghead.io tutorial, and was much heavier on Redux-style things. Later iterations became simpler, eventually evolving into the current version.
3. Can I use this in [React, Vue, Svelte ... ]?
Yes.
No restrictions; only Javascript.
This is, ultimately, a plain JS object. You can use it anywhere you can use JS and need a dynamic in-memory state. It can be restricted to a single component, or used for an entire UI application, or in a command line program. See the examples for UI frameworks.
4. Why not just use redux?
This is much, *much* simpler to learn and implement.
- ~~Because clearly, Javascript needs MOAR solutions for solved problems.~~
- Not everyone needs redux. Not everyone needs raphsducks, either
- In fact, not everyone needs state.
Redux does a good deal more than raphsducks's humble collection of lines. I wanted something lightweight with the pub/sub API, which would allow me to quickly extend an application's state without getting into fist-fights with opinionated patterns.
As with many JS offerings, I acknowledge that it could be the result of thinking about a problem wrong: use at your discretion.
Development
The core class remains a plain JS object. All dependencies are defined in the package.json
file: as of v2
, the library includes an rxjs
dependency.
git clone https://github.com/JACK-COM/raphsducks.git && npm install
Run tests:
npm test