core-flux

½kb functional flux utility. Control the flow of state data between subscribers.

Usage no npm install needed!

<script type="module">
  import coreFlux from 'https://cdn.skypack.dev/core-flux';
</script>

README

Core Flux

½kb functional flux utility. Control the flow of state data between subscribers.


Version License Circle CI status (main) bundle size devDependencies

See a demo of Core Flux in action!

Install

NPM / Yarn

$ npm i core-flux
$ yarn i core-flux

CDN

The CDN puts the library on window.CoreFlux.

<!-- The unminified bundle for development -->
<script
  type="text/javascript"
  src="https://cdn.jsdelivr.net/npm/core-flux@1.1.3/dist/core-flux.js"
  integrity="sha256-9u61rcLUN8tcguIWY/5DOjRDibuAg6Ix1d/zYrnLzcY="
  crossorigin="anonymous"
></script>

<!-- Minified/uglified bundle for production -->
<script
  type="text/javascript"
  src="https://cdn.jsdelivr.net/npm/core-flux@1.1.3/dist/core-flux.min.js"
  integrity="sha256-Pdr3Zs1hWFBCdbQYRxMg4ODH00MY2s++8Kwp0+UseGc="
  crossorigin="anonymous"
></script>

API

createStore(initialSate, reducer, bindSubscriber, bindState)

The one and only export of Core Flux. Use it to create a store instance. You can create as few or as many stores as your heart desires! They will all be independent from one another.

The function requires all four of its arguments, as shown here:

// foo-store.js

import { createStore } from "core-flux"
import { reducer, bindSubscriber, bindState } from "./foo-bindings"

const initialState = {
  foo: [],
  bar: { baz: 0, beep: "hello" },
}

const { subscribe, dispatch } = createStore(
  initialState,
  reducer,
  bindSubscriber,
  bindState
)

export { subscribe, dispatch }

Once a store is created, you'll be able to add subscriptions with subscribe and request state updates with dispatch.

Bindings

Here's a breakdown of each binding needed when initializing a new store:

reducer(state, action)

state (object): A copy of the current state object.
action ({ type: string, payload: object }): The dispatched action type and its payload.

Creates a new version of state and returns it, based on the type and payload. If the return value is falsy, the update process ends.

bindSubscriber(newSubscription, state)

newSubscription ([subscriber, data]): A tuple containing the subscribed object and its state-relational data.
state (object): A copy of the current state object.

Called after a new subscribe call is made and a subscription has been added to the store. Use it to set initial state on the new subscriber based on the data defined by your subscriber.

bindState(subscriptions, reducedState, setState)

subscriptions (subscription[]): An array containing all subscriptions.
reducedState (object): The state object as returned by the reducer.
setState (function):

Called after the reducer has processed the next state value. Use it to set the reduced state back to subscribers and back to the store.

subscribe(subscriber, data)

Adds a subscription to your store. It will always be tied to a single store, and subsequently state object.

import { subscribe } from "./foo-store"

class FooItems {
  constructor() {
    subscribe(this, ["foo"])
  }

  get items() {
    return this.foo
  }
}

In the above example, we've designed the subscriber, the FooItems class, to declare an array of strings correlating to properties in the store's state. If you're from the Redux world, this is akin to "connecting" a consumer to a provider via higher-order function/component.

After the subscribe call is made, your bindSubscriber function will be called where you can pass along the default values as you see fit.

In general, you should try to use a simple data structure as the second argument to subscribe; this ensures your bindings have generic and consistent expectations.

dispatch(type, payload)

Requests a state change in your store.

We can extend the previous example with a setter to call dispatch:

import { subscribe, dispatch } from "./foo-store"

class FooItems {
  constructor() {
    subscribe(this, ["foo"])
  }

  get items() {
    return this.foo
  }

  addItem(item) {
    dispatch("ADD_ITEM", { item })
  }
}

const fooBar = new FooItems()
fooBar.addItem("bop")

Now when the addItem method is called, Core Flux will pass along the action type and payload to your reducer.

The reducer could have a logic branch on the action type called ADD_ITEM which adds the given item to state, then returns the resulting new state (containing the full items list).

Finally, the result would then be handed over to your bindState binding.

Much like in subscribe, it's best to maintain data types in the payload so your reducer can have consistent expectations.

Exposing the store

For utility or debugging reasons, you may want to look at the store you're working with. To do so, you can use the __data property when creating a store.

const fooStore = createStore(initialState, reducer, bindSubscriber, bindState)

window.fooStoreData = fooStore.__data

console.log(window.fooStoreData) // { state: {...}, subscriptions: [...] }

Data model

Core Flux has a relatively simple data model that you should understand when creating your bindings.

Here is how state looks in all cases:

Store {
  state: { ... },
  subscriptions: [
    [subscriber, data],
    [subscriber, data],
    [subscriber, data],
    // ...
  ]
}

Each item in subscriptions contains a subscriber and some form of data that informs a relationship between state and subscriber.

NOTE: You define data in the above model, be it an object, array, string; it can be anything you want. Ultimately, you're responsible for communicating state relationships to subscribers.

Data flow

Here is the general lifecycle of subscribing to the store & dispatching a state update.

  • subscribe > bindSubscriber
  • dispatch > reducer > bindState