3.0.3 • Published 7 months ago

@jackcom/raphsducks v3.0.3

Weekly downloads
1
License
WTFPL
Repository
github
Last release
7 months ago

Raph's Ducks v3

UPDATES:

  • Version 1.X.X simplifies the library and introduces breaking changes. If you're looking for the 0.X.X documentation (I am so sorry), look here,
  • Version 1.1.X adds typescript support, and a new subscribeOnce function (see below)
  • Version 2.X.X introduces rxjs under the hood
  • Version 3.X.X replaces rxjs with immutablejs for maximum profit


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.
  • 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:

1) Define a state, and 2) 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: [],
    somethingTruthy: false,
    counter: 0,
    nullableString: ''
}

// 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 expect null 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 somethingTruthy 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. You can supply this directly to
// `createState` unless you're (e.g.) generating it from a function
const initialState = {
    todos: [] as ToDo[], // require an array of `ToDo` objects
    somethingTruthy: false, // boolean (inferred)
    counter: 0, // number (inferred)
    nullableString: '' as string | null // will allow `null` for this key
}

// Create an instance with your state definition
const store = createState(initialState);

// update select keys
store.multiple({
   somethingTruthy: true,
   counter: 3,
}); 
// Check results
store.getState().somethingTruthy;    // true
store.getState().counter;       // 3

// Or use destructuring
const { somethingTruthy, counter} = store.getState()
console.log(somethingTruthy); // true
console.log(counter); // 3
ii. Initial State Type Definitions

You can optionally create a type-def for the entire state, though this gets unwieldy to maintain (since you need to update the type-def along with the initial state object). Inline definitions are recommended (see above).

// IMPORTANT: DO NOT initialize properties as "undefined", or you'll never hear the end of it.
type MyState = {
  todos: ToDo[];
  somethingTruthy: boolean;
  counter: number;
  nullableString: stringl
};

// A single `To do` object (e.g. for a to-do list)
type ToDo = { title: string, value: boolean };

// OPTION 1: Type-cast your initial state to get TS warnings for missing properties.
const initialState: MyState = { ... };
const store = createState(initialState);

// OPTION 2: Type-cast the `createState` function itself
const store = createState<MyState>( /* initialState */ );

// Now you have type definitions and editor hints:
store.somethingTruthy; // (v: boolean) => void;

// And you can get typescript warnings when supplying the wrong value
store.somethingTruthy("A string"); // TS Error: function expects boolean

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.

// Update one key at a time
store.todos([{ title: "Write code", value: true }]); // notifies subscribers
store.somethingTruthy(false); // notifies subscribers

// Update several keys. Subscribers are notified once per 'multiple' call.
store.multiple({
    todos: [{ title: "Write code", value: true }],
    somethingTruthy: true,
}); // notifies subscribers

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 (CORRECT WAY)
const oldTodos = store.getState().todos
const newTodos = [...oldTodos, { title: "New task", value: false }]

store.multiple({
    todos: newTodos,
    somethingTruthy: true,
});

Listening to your state instance

You can subscribe for updates. Your subscriber should take two values: the updated state values, and a list of just-updated state property names.

Every subscription returns an unsubscribe function. You can call it when you no longer need to listen for updates, or (for front-end apps) use it to clean up when a component is removed from the DOM.

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, although you may have to check the updated object to see if it has the 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 see in the state. See below.

Hint: the listener handler is the same in all subscribe functions. It always accepts two arguments: the updated state object-literal, and a list of keys that were just updated.

Disposable (one-time) subscription

subscribeOnce allows you to listen until a specfic key is updated (or just until the next state update happens). It will auto-unsubscribe depending on how you use it.

Listen until the next state update

You can wait for the next state update to trigger something else. This assumes that you don't care what is in state, as long as some other part of your application updated it.

const unsubscribe = store.subscribeOnce(() => {
    doSomethingElse();
});

// Cancel the trigger by unsubscribing:
unsubscribe(); // 'doSomethingElse' won't get called.
One-time subscription to a specific key

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;
    doSomethingWith(todos);
}, 'todos');

// You can pre-emptively skip the state-update by unsubscribing first:
unsubscribe(); // 'doSomethingElse' won't get called when state updates
One-time subscription to a specific value

Listen until a specific item gets updated with a specific value, then use it.\ As above, the value is guaranteed to be on the updated state object. We'll use state.counter for our example.

const unsubscribe = store.subscribeOnce(
  // `state.counter` >= 3 here because of the extra parameters below
  (state) => {
    // no more updates after this gets triggered once
    const counter = state.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 Subscription

You can target updates to very specific keys. Like subscribeOnce, you can refine how these updates are handled.

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 VALUES on 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(
  // `state.counter` >= 3 here because of the extra parameters below
  (state) => {
    // "todos" OR "counter" may be undefined. If they aren't, they will meet
    // the conditions specified in our value-checker
    const {todos, counter} = state; 
    if (todos) doSomethingWith(todos);
    if (counter) doSomethingElseWith(counter);
  }, 

  // KEYS: tell us when "state.counter" OR "state.todos" changes
  ['todos', 'counter'], 
  
  // VALUE-CHECKER: make sure our keys have the values we want
  (key, value) => {
    // update only when "state.counter" is 3 or greater
    if (key === "counter") return value >= 3; 
    
    // update 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 updates

Preserving 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 state instance 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 (Class)

  • State instance returned from createState(). View full API and method explanations here.
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.

Hint: the Store manages your state 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 updated state 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:

// 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. localStorage or sessionStorage).
  • 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, and subscribe methods (for getting a copy of current state, and listening to updates).
    • subscribe even returns an unsubscribe function!

How is it different from Redux?

  • You can use it in a pure NodeJS environment
  • No Actions, dispatchers, or reducers
  • 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?

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.

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.


4. Why not just use redux?

Because this is 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 a pub/sub API. It allows me to quickly extend an application's state without getting into fist-fights with opinionated patterns.


5. Anything else I should know?

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 added rxjs.
  • In v3, rxjs was replaced with ImmutableJS
$. git clone <https://github.com/JACK-COM/raphsducks.git> && npm install

Run tests:

$. npm test
3.0.3

7 months ago

3.0.2

8 months ago

3.0.1

2 years ago

3.0.0

2 years ago

2.0.7

2 years ago

2.0.3

3 years ago

2.0.2

3 years ago

2.0.4

3 years ago

2.0.6

3 years ago

2.0.1

3 years ago

2.0.0

3 years ago

1.2.0

3 years ago

1.1.6

4 years ago

1.1.5

4 years ago

1.1.4

4 years ago

1.1.1

4 years ago

1.1.3

4 years ago

1.1.2

4 years ago

1.1.0

4 years ago

1.0.0-rc1

4 years ago

1.0.4

4 years ago

0.7.1

4 years ago

0.7.0

4 years ago

0.6.0

4 years ago

0.5.3

6 years ago

0.5.2

7 years ago

0.5.1

7 years ago

0.5.0

7 years ago

0.4.3

7 years ago

0.4.2

7 years ago

0.4.1

7 years ago

0.4.0

7 years ago

0.3.0

7 years ago

0.2.0

7 years ago

0.1.11

8 years ago

0.1.10

8 years ago

0.1.9

8 years ago

0.1.8

8 years ago

0.1.7

8 years ago

0.1.6

8 years ago

0.1.4

8 years ago

0.1.3

8 years ago

0.1.2

8 years ago

0.1.1

8 years ago