Simple replacement for Redux in ES6

Posted on


The idea about this code is that it’s a full replacement of Redux — in 22 lines of code.

let state = null
const beforeSetStateCallbacks = []
const afterSetStateCallbacks = []

export const beforeSetState = (fn) => beforeSetStateCallbacks.push(fn)
export const afterSetState = (fn) => afterSetStateCallbacks.push(fn)

export const setState = async (key, value) => {
  if (state === null) state = {}
  // QUESTION: Should I use Promise.all for performance reason?
  // Note that the order might be important though
  for (const fn of beforeSetStateCallbacks) await fn(key, value)
  state[key] = value
  for (const fn of afterSetStateCallbacks) await fn(key, value)

export const stateEmpty = () => {
  return !state

export const getState = (key) => {
  if (!state) return null
  return state[key]

There is no dispatch() because the whole actions/reducers madness melts away, simple functions are meant to modify the state and then call (for example) setState('userInfo', userInfo).

Example usage is here:!/lacy-ornament

The rundown:

  • index.html contains this:
<script type="module" src="./one.js"></script>

The first line defines a custom element my-one. The second line places that element.

  • StateMixin.js contains this:
import { setState, getState, afterSetState, stateEmpty } from './reducs.js'

export const StateMixin = (base) => {
  return class Base extends base {
    constructor () {

      if (stateEmpty()) {
        setState('basket', { items: [], total: 0 })

      this.basket = getState('basket')

      afterSetState((k, value) => {
        this[k] = { ...value }

Basically, something is added to the constructor where if the state isn’t yet defined, it gets assigned a default initial state.
Then, assigningthis.basket makes sure that the basket property of the element is current, and afterSetState() will make sure that further modifications to the basket will also update this.basket.

This means that any element that has this.basket as a property will get updated when this.basket is changed.

  • one.js contains this:
import { LitElement, html } from 'lit-element/lit-element.js'
import { StateMixin } from './StateMixin.js'
import { addItemToBasket } from './actions.js'

// Extend the LitElement base class
class MyOne extends StateMixin(LitElement) {
  static get properties () {
    return {
      basket: {
        type: Object,
        attribute: false
  render () {
    return html`
      <p>Total items: ${}</p>
        ${ => html`
      <input type="text" id="description">
      <button @click="${this._addItem}">Add</button>
  _addItem () {    
    const inputField = this.shadowRoot.querySelector('#description')
    addItemToBasket( { description: inputField.value })
    inputField.value = ''
// Register the new element with the browser.
customElements.define('my-one', MyOne)

This is a very minimalistic element, where a few interesting things happen. First of all, it’s mixed with StateMixin.js, which will ensure that the constructor deals with status and registration for changes. It also defines a basket property, which means that when the basket is changed, the element will re-render.

Finally, _addItem() will run the action addItemToBasket() which is the only action defined.

  • actions.js contains this:
import { getState, setState } from './reducs.js'
export const addItemToBasket = (item) => {
  const basket = getState('basket')

   basket.items. push(item) = basket.items.length
   basket.items = [ ...basket.items ]

   setState('basket', basket)

This is simple: first of all, the state is loaded with getState(). Then, it’s modified. Note that lit-html only re-renders changed things. So, basket.items is re-assigned. Finally, the state is set with setState().

Note: you might have multiple branches in your state: basket, userInfo, appConfig, and so on.


  • If I use Promise.all, I will lose the certainty that the calls are called in order. Do you think I should still do it?
  • Is there a way to prevent the check on state every single time in setState() and stateEmpty()? The idea is that stateEmpty() returns false if the state has never been initialised.
  • How would you recommend to implement an “unlisten” function here? As in, what’s the simplest possible path to provide the ability to stop listening?
  • Since this is indeed a working 100% replacement on Redux (in 22 lines), shall I name the functions more ala Redux? Redux has “subscribe”, but I like giving the option to subscribe to before and after the change. Maybe subscribe() and subscribeBefore()?


I may or may not have seen a certain movie in the cinema, but the Batman in me wants to say this:

enter image description here

Not to mention that semicolons don’t hurt either.

Also, per @Blindman67, it seems that you are pushing complexity down to the callers. It seems to me a number of callbacks would only want to run for a given key, forcing callers to check the key value is not good design.

For this part:

Is there a way to prevent the check on state every single time in
setState() and stateEmpty()?

I would declare state like let state = {};
Then you dont need to check in setState, because state is already an Object.
You would write stateEmpty like this:

export const stateEmpty = () => {
  return !Object.keys(state).length;

export const getState = (key) => {
  return state[key]

This would save you 2 lines, and avoids the akward null value.

The Joker in me considers your beforeSetStateCallbacks and afterSetStateCallbacks as just two parts of the same coin. I have not tested this, but the below should be both possible and cleaner;

let state = {};
const batch = [(key,value)=>state[key] = value];

export const beforeSetState = (fn) => batch.unshift(fn);
export const afterSetState = (fn) => batch.push(fn);

export const setState = async (key, value) => {
  for (const f of batch){
    await f(key, value);

In this vein, this question becomes easier:

How would you recommend to implement an “unlisten” function here?

If the subscriber remembers the function they used to subscribe you could go like this, because now you dont care whether the caller listens to before or after.

export const unhook = (fn) => batch.splice(0, batch.length, ...batch.filter(f=>f!=fn));

The last item I want to mention is a graceful exit. Your code is short, because for one thing you trust that the caller will always provide a function for fn. Consider adding more type checks, otherwise the stack-trace will end frequently in your code.

After @konjin’s great answer, I ended up with this wonderful code:

const state = {}
let beforeCallbacks = []
let afterCallbacks = []
let globalId = 0

const deleteId = (id, type) => {
  type === 'a'
    ? afterCallbacks = afterCallbacks.filter(e => !== id)
    : beforeCallbacks = beforeCallbacks.filter(e => !== id)

export const register = (fn, type = 'a') => {
  const id = globalId++
  (type === 'a' ? afterCallbacks : beforeCallbacks).push({ fn, id })
  return () => deleteId(id, type)

export const setState = async (key, value) => {
  for (const e of [...beforeCallbacks, { fn: () => { state[key] = value } }, ...afterCallbacks]) {
    const v = e.fn(key, value)
    if (v instanceof Promise) await v
export const stateEmpty = () => !Object.keys(state).length
export const getState = (key) => state[key]

I used his idea in setState() to create an array where state[key] = value is sandwiched between the before and after calls.

I followed his advice of assigning {} to state, and adding curly brackets like Batman said 😀

I implemented de-registration by adding an ID to each one, rather than deleting the function, as I want to make sure I can assign the same function and de-register it without side effects.

His answer IS the accepted answer.


Leave a Reply

Your email address will not be published. Required fields are marked *