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()
?
-
\$\begingroup\$ @Merc, please add a testable jsfiddle.net link (or alike) with code (and sample object) \$\endgroup\$RomanPerekhrest– RomanPerekhrest2019年10月27日 13:38:26 +00:00Commented Oct 27, 2019 at 13:38
-
\$\begingroup\$ OK doing it now \$\endgroup\$Merc– Merc2019年10月27日 21:14:20 +00:00Commented Oct 27, 2019 at 21:14
-
\$\begingroup\$ OK done. @RomanPerekhrest doing this was a JOB AND A HALF :D I hope what I wrote makes sense. I really do look (humbly) forward to comments and reviews. \$\endgroup\$Merc– Merc2019年10月27日 22:46:36 +00:00Commented Oct 27, 2019 at 22:46
-
1\$\begingroup\$ I just saw the tag "reinventing-the-wheel". I think we should also add the tag "simplifying-an-overcomplex-wheel" :D \$\endgroup\$Merc– Merc2019年10月27日 23:18:00 +00:00Commented Oct 27, 2019 at 23:18
-
\$\begingroup\$ I am not a redux user so my knowledge is limited. However I have some questions? There is no state history?, The sate is being mutated and holding references, not copies. Are you relying on the components to ensure only copies are passed and that the callbacks do not mutate the state value? The callbacks (Before, and After) are state global, this seams very clumbersum for large states models. Is the need to be 22 lines forcing the code using your mini redux to be longer? \$\endgroup\$Blindman67– Blindman672019年10月28日 10:56:14 +00:00Commented Oct 28, 2019 at 10:56
2 Answers 2
I may or may not have seen a certain movie in the cinema, but the Batman in me wants to say this:
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.
-
\$\begingroup\$ GOLDEN. I will implement every single suggestion here. Now... how do I do this? I don't want to change the question. Shall I add the updated code as a trailing update? Sorry, I am a StackOverflow guy, I don't spend enough time here at CodeReview! \$\endgroup\$Merc– Merc2019年10月28日 13:15:40 +00:00Commented Oct 28, 2019 at 13:15
-
\$\begingroup\$ I love your suggestion for batched callback, and how it starts with one prefefined call in the middle. My only problem here is that developers will expect
beforeSetState()
calls to be run in the order they are defined, whereas with your code they will be run in reversed order. Right? \$\endgroup\$Merc– Merc2019年10月28日 13:22:34 +00:00Commented Oct 28, 2019 at 13:22 -
\$\begingroup\$ @Merc after your question has been answered you should NOT change your question! \$\endgroup\$Blindman67– Blindman672019年10月28日 13:23:38 +00:00Commented Oct 28, 2019 at 13:23
-
\$\begingroup\$ About unlistening, a user could want to listen to the same function twice, and only desire to "unlisten" to a specific one. Redux does it by making sure that register() returns a function that will remove that registration. But... I am having trouble finding a nice, reliable way to implement that \$\endgroup\$Merc– Merc2019年10月28日 13:25:20 +00:00Commented Oct 28, 2019 at 13:25
-
\$\begingroup\$ I need to think on the order and unhook points, they are both valid. \$\endgroup\$konijn– konijn2019年10月28日 13:42:54 +00:00Commented Oct 28, 2019 at 13:42
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 => e.id !== id)
: beforeCallbacks = beforeCallbacks.filter(e => e.id !== 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 :D
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.
Thanks!
Explore related questions
See similar questions with these tags.