@jackcom/raphsducks
v3.1.0
Published
A lightweight pubsub (publish-subscribe) state manager.
Maintainers
Readme
Raph's Ducks v3
- 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)
This library can be used alone, with or without a UI framework, and in combination with other state managers. Here's what you do:
- Define a state, and
- Use it.
If it isn't the simplest state-manager you have ever encountered, I'll ...
I'll eat my very ~~javascript~~ typescript.
- Raph's Ducks v3
Installation
npm i -s @jackcom/raphsducksQuick start
The following snippet is a high-level overview. If you're working with typescript, see Use TypeScript
import createState from '@jackcom/raphsducks';
// The state instance you will actual use.
const store = createState({
todos: [],
truthy: false,
counter: 0,
nullableString: ''
});
// 1. Update a key at a time
store.todos([ /* ... */ ]);
store.truthy(true);
store.counter(1);
// 2. Update multiple keys at a time
store.multiple({ todos: [/* ... */], counter: 4 })
// 3. Check for changes
const { counter, todos } = store.getState();
// 4. Subscribe to changes
const unsubscribe = store.subscribe((state) => {
const { counter, todos } = state;
// ... do something with changes
})Define your state
raphsducks exports a single function, createState. Use it to create an object that you can observe or update in different ways from your state representation.
import createState from '@jackcom/raphsducks';
// An object-literal you supply for your initial state.
const initialState = {
todos: [],
truthy: false,
counter: 0,
nullableString: ''
}
// Your state instance (for subscribing and updating the initial state).
const store = createState(initialState);
// (OPTIONAL) export for use in other parts of your app
export default store;[!NOTE] A typescript key initialized with
nullwill always expectnullas an update value. To limit type assertion errors, use<type> | nullfor falsy types.Example:
{ myString: '' as string | null }eliminates type errors when you callstore.myString(null).This is not an issue if you are using vanilla JavaScript.
In the example above, both todos and truthy will become functions on store.
Use TypeScript
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.
Inline type definitions (recommended)
// A single `To do` object (e.g. for a to-do list) type ToDo = { title: string, description?: string, done: boolean }; // Create an instance with your initial state. This example uses inline type // definitions. const store = createState({ todos: [] as ToDo[], // require an array of `ToDo` objects truthy: false, // boolean (inferred) counter: 0, // number (inferred) nullableString: '' as string | null // will allow `null` for this key });Initial State Type Definition
You can optionally create a type definition for the entire state. This is not recommended because you need to update the type and the initial state object.
Example 1: Type-cast your initial state to get TS warnings for missing properties.
// IMPORTANT: Use "<value> || null" for falsy values. type MyState = { todos: { title: string, value: boolean }[]; truthy: boolean; counter: number; nullableString: string; }; const initialState: MyState = { todos: [], truthy: false, counter: 0, nullableString: null }; const store = createState(initialState);Example 2: Type-cast the
createStatefunction itselftype MyState = { todos: { title: string, value: boolean }[]; truthy: boolean; counter: number; nullableString: string; }; const store = createState<MyState>( /* initialState */ ); store.truthy("A string"); // TS Error: function expects boolean
Update 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.
// Ex. 1: Update one key at a time
// Notify subscribers that "todos" was changed
store.todos([{ title: "Write code", value: true }]);
// Notify subscribers that "truthy" was changed
store.truthy(false);
// Ex. 2: Update several keys at once. Subscribers are notified once per 'multiple' call.
// Notify subscribers that "truthy" and "todos" were changed
store.multiple({
todos: [{ title: "Write code", value: true }],
truthy: true,
}); [!WARNING] Update object properties carefully (e.g. merge
Arrayproperties before supplying them inargs). The library overwrites key values with what you provide.
// Updating an array property (CORRECT WAY)
const oldTodos = store.getState().todos
const newTodos = [...oldTodos, { title: "New task", value: false }]
store.multiple({
todos: newTodos,
truthy: true,
});Subscribe to state updates
Your state subscriber (or listener) takes two values: the updated state values, and a list of just-updated state property names.
- The updated
stateobject-literal. - list of keys that were just updated.
Every subscription returns an unsubscribe function. Use this to stop listening for updates, or to clean up when a frontend component is removed from the DOM.
// An example local reference for the values you want from state
let myTodos = [];
// Create an unsubscriber by subscribing to a state instance
const unsubscribe = store.subscribe((state, updatedKeys) => {
// Check if a value you care about was updated.
if (updatedKeys.includes("todos")) myTodos = state.todos
});
// stop listening to state updates
unsubscribe();state.subscribe() listens to every change that happens to your state. However, you may have to check the updated object to see if the new state has the values you want.
There are other ways to subscribe to your state instance. Some of them allow you to guarantee what values should be in state before calling your listener.
Disposable subscriptions
subscribeOnce allows you to listen until a specfic key (or any key) is updated. It will auto-unsubscribe after calling your listener.
Listen for only the next state update
Wait for the next state update to trigger an action, regardless of what gets updated:
const unsubscribe = store.subscribeOnce(() => { doSomethingElse(); }); // Cancel the trigger by unsubscribing: unsubscribe(); // 'doSomethingElse' won't get called.Subscribe only once to a specific key
Listen until a specific item gets updated. The value is guaranteed to be on the updated state object. This example uses a
state.todosarray:const unsubscribe = store.subscribeOnce((state) => { const todos = state.todos; doSomethingWith(todos); }, 'todos'); // You can pre-emptively skip the state-update by unsubscribing first: unsubscribe(); // 'doSomethingElse' won't get called when state updatesSubscribe once to a specific value
Listen until a specific item gets updated with a a specific value.
The value is guaranteed to be on the updated state object. We'll usestate.counterfor our example.const unsubscribe = store.subscribeOnce( // `state.counter` >= 3 here because of the extra parameters below. // This gets called once. ({ counter }) => doSomethingWith(counter), // tell us when "state.counter" changes 'counter', // only call the listener if "state.counter" is 3 or greater (count) => count >= 3 ); // Pre-emptively skip the state-update by unsubscribing first: unsubscribe(); // 'doSomethingElse' won't get called when state updates
Tactical Subscriptions
Use subscribeToKeys to listen for updates to specific keys.
Listen for ANY change to specific keys
Trigger updates whenever your specified keys are updated. At least one value is guaranteed to be present, because the state object can be updated in any order by any part of your app.
const unsubscribe = store.subscribeToKeys(
(state) => {
// This will continue to receive updates for both keys until you unsubscribe
const {todos, counter} = state; // "todos" OR "counter" may be undefined
if (todos) doSomethingWith(todos);
if (counter) doSomethingElseWith(counter);
},
// Only tell us when either of these keys changes
['todos', 'counter']
);
// Unsubscribe from updates when done:
unsubscribe(); [!Note] BOTH values will be present if your app does a
store.multiple( ... )update that includes both keys.
Listen for SPECIFIC CHANGES to specific keys
You can mitigate uncertainty by providing a value-checker. While it doesn't guarantee that your keys will be present, you may at least ensure that the keys have the values you want on them.
const unsubscribe = store.subscribeOnce(
// LISTENER: Run this when state.todos and/or state.counter is changed
(state) => {
// EITHER "todos" OR "counter" may be undefined. At least one key
// is guaranteed to be present.
const {todos, counter} = state;
// If "todos" is present, it will have more than 3 todos (see VALUE-CHECKER below)
if (todos) doSomethingWith(todos);
// If "counter" is present, it will be >= 3 (see VALUE-CHECKER below)
if (counter) doSomethingElseWith(counter);
},
// KEYS: listen for changes to "state.counter" OR "state.todos"
['todos', 'counter'],
// VALUE-CHECKER: make sure the keys have specific values
(key, value) => {
// call LISTENER only when "state.counter" changes to 3 or greater
if (key === "counter") return value >= 3;
// call LISTENER when state has more than 3 todos added
if (key === "todos") return value.length > 3;
}
);
// Pre-emptively skip the state-update by unsubscribing first:
unsubscribe(); // 'doSomethingElse' won't get called when state updatesPreserving state
Since this is an unopinionated library, you can preserve your state data in any manner that best-fits your application. The .getState() method returns a plain Javascript Object, which you can JSON.stringify and write to localStorage (in a browser) or to some database or other logging function. The ApplicationStore class now provides a serialize method that returns a string representation of your state:
store.serialize(); // JSON string: "{\"counter\": 0 ... }"Of course, this is only useful if your objects are serializable. If you store complex objects with their own methods and such -- and you can -- this will not preserve their methods.
LocalStorage with serialize
// EXAMPLE: save and load user state with localstorage
localStorage.setItem("user", store.serialize()); // save current state
// EXAMPLE Load app state from localstorage
const stateStr = localStorage.getItem("user");
if (user) store.multiple(JSON.parse(stateStr));You can use the return value of serialize wherever it makes the most sense for your app.
Reference
createState
createState(state: { [x:string]: any }): ApplicationStore- Default Library export. Creates a new
stateinstance using the supplied initial state.
Parameters:initialState: Your state-representation (an object-literal representing every key and initial value for your global state).
- Returns: a state instance.
ApplicationStore
State instance returned from createState(). View full API and method explanations in API.
class ApplicationStore {
getState(): StoreInstance;
multiple(changes: Partial<StoreInstance>): void;
reset(clearSubscribers?: boolean): void;
serialize(): string;
subscribe(listener: ListenerFn): Unsubscriber;
subscribeOnce<K extends keyof StoreInstance>(
listener: ListenerFn,
key?: K,
valueCheck?: (some: StoreInstance[K]) => boolean
): void;
subscribeToKeys<K extends keyof StoreInstance>(
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;
}Store Instance
An ApplicationStore instance with full subscription capabilities. This is distinct from your state representation.
[!TIP] The
Storemanages yourstate representation.
State Representation
The plain JS object literal that you pass into createState.
This object IS your application state: it contains any properties you want to track and update in an application. You manage your state representation via the Store Instance.
Listener Functions
A listener is a function that reacts to state updates. It expects one or two arguments:
state: { [x:string]: any }: the updatedstateobject.updatedItems: string[]: a list of keys (stateobject properties) that were just updated.
Example Listener
A basic Listener receives the updated application state, and the names of any changed properties, as below:
// Assume you have a local copy of some state value here
let localTodos = [];
function myListener(newState: object, updtedKeys: string[]) {
// You can check if your property changed
if (newState.todos === localTodos) return;
// or just check if it was one of the recently-updated keys
if (!updtedKeys.includes("todos")) return;
// `state.someProperty` changed: do something with it! Be somebody!
localTodos = newState.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.
localStorageorsessionStorage). - 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
createStatefunction helper for instantiating the state - Uses
getState, andsubscribemethods (for getting a copy of current state, and listening to updates).subscribeeven returns an unsubscribe function!
How is it different from Redux?
- You can use it in a pure NodeJS environment
- No
Actions,dispatchers, orreducers - You can use with any UI framework like ReactJS, SvelteJS, or Vue
- ~~No serialization~~ You can request the current state as a JSON string, but the instance doesn't care what you do with it.
1. Why did you choose that name?
I didn't. But I like it.2. Does this need React or Redux?
NopeThis 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.This is just a JS class. It can be restricted to a single component, or used for an entire UI application, or even in a command line program. I have personally used it in NodeJS projects, as well as to pass data between a React App and JS Web Workers.
No restrictions; only Javascript.
For a ReactJS example, see ReactJS State Subscription via useEffect. For usage with VueJS, see VueJS mixin example
4. Why not just use redux?
Because this is
1. Smaller
2. Simpler to learn
3. Simpler 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 a pub/sub API.
5. Anything else I should know?
- Keep your state simple.
- For example, put user info in one state, and user-created content (such as blog posts, or a shopping cart) in another. This keeps your updates zippy, and limits the number of subscribers to each state instance.
- Only subscribe when you need to.
- Use
getStateto read and act upon state values. Subscribe when you need to respond to a state update (for example, changing a UI value, or triggering some other action).
- Use
- 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, now with a single external dependency:
- In
v2, the library addedrxjs. - In
v3,rxjswas replaced withImmutableJS
$. git clone <https://github.com/JACK-COM/raphsducks.git> && npm installRun tests:
$. npm testRelease notes
- Version
1.X.Xsimplifies the library and introduces breaking changes. If you're looking for the0.X.Xdocumentation (I am so sorry), look here,- Version
1.1.X
- Adds
typescriptsupport- Adds new
subscribeOncefunction- Version
2.X.X
- Introduces
rxjsunder the hood- Updates
subscribeToKeys- Version
3.X.X
- Replaces
rxjswithimmutablejsfor maximum profit
