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: https://glitch.com/edit/#!/lacy-ornament
The rundown:
index.html
contains this:
<script type="module" src="./one.js"></script>
<my-one></my-one>
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 () {
super()
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>Basket</p>
<p>Total items: ${this.basket.total}</p>
<p>Items:</p>
<ul>
${this.basket.items.map(item => html`
<li>${item.description}</li>
`)}
</ul>
<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.total = 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.
Questions:
- 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()
andstateEmpty()
? 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()
andsubscribeBefore()
?