E~wee~ctor: writing tiny Effector from scratch #3 — Simple API methods
Victor Didenko
Posted on April 4, 2020
Hi, folks!
In this article I want to implement some simple Effector API functions. But before we start, let's improve one thing.
You might have noticed, that we create auxiliary node and add it to the next
array of other node quite often, like this:
event.map = fn => {
const mapped = createEvent()
// create new node
const node = createNode({
next: [mapped.graphite],
seq: [compute(fn)],
})
// add it to the event's next nodes
event.graphite.next.push(node)
return mapped
}
Let's improve createNode
function, so it will do it for us:
export const getGraph = unit => unit.graphite || unit
const arrify = units =>
[units]
.flat() // flatten array
.filter(Boolean) // filter out undefined values
.map(getGraph) // get graph nodes
export const createNode = ({ from, seq = [], to } = {}) => {
const node = {
next: arrify(to),
seq,
}
arrify(from).forEach(n => n.next.push(node))
return node
}
I've renamed parameter next
to to
, and added new parameter from
, accepting previous nodes.
getGraph
helper function gives us ability to pass both units and nodes, without taking care of field .graphite
. Also, with arrify
helper function we can pass single unit or array of units to the from
and to
parameters.
Now any createNode
call should be more readable:
from
node →seq
uence of steps →to
node
With this change we can rewrite example above as following:
event.map = fn => {
const mapped = createEvent()
// create new node
// and automatically add it to the event's next nodes
createNode({
from: event,
seq: [compute(fn)],
to: mapped,
})
return mapped
}
I wont show you all diffs of all createNode
function occurrences, the changes are trivial, you can make them yourself, or check commit by the link in the end of the article, as usual :)
Let's move on to the API methods!
forward
export const forward = ({ from, to }) => {
createNode({
from,
to,
})
}
That's simple :)
⚠️ Well, not quite so, Effector's Forward returns so called Subscription, to be able to remove connection. We will implement subscriptions in later chapters.
Remember we can pass array of units/nodes to createNode
function, so forward
can handle arrays automatically!
merge
export const merge = (...events) => {
const event = createEvent()
forward({
from: events.flat(), // to support both arrays and rest parameters
to: event,
})
return event
}
merge
creates new event and forwards all given events to that new one.
⚠️ Effector's merge
supports only arrays. I've added rest parameters support just because I can ^_^
split
const not = fn => value => !fn(value) // inverts comparator function
export const split = (event, cases) => {
const result = {}
for (const key in cases) {
const fn = cases[key]
result[key] = event.filter(fn)
event = event.filter(not(fn))
}
result.__ = event
return result
}
split
function splits event into several events, which fire if source event matches corresponding comparator function.
"It may seem difficult at first, but everything is difficult at first."
— Miyamoto Musashi
So, take your time understanding this function.
And here is diagram of split
:
Or in a less detailed, but more beautiful form of a tree, split
is actually looks like a recursive binary splitting:
createApi
export const createApi = (store, setters) => {
const result = {}
for (const key in setters) {
const fn = setters[key]
result[key] = createEvent()
store.on(result[key], fn)
}
return result
}
createApi
function is just a simple factory for events, and it auto-subscribes given store on each one of them.
is
We can distinguish events and stores by doing typeof
(events are functions and stores are plain objects). But this approach has a flaw – when we will implement effects it will fail, because effects are functions too. We could go further and check all properties – this is called duck typing. But Effector does that very simple – each unit has a special field kind
:
export const createEvent = () => {
// --8<--
+ event.kind = 'event'
return event
}
export const createStore = defaultState => {
// --8<--
+ store.kind = 'store'
return store
}
And with this field kind
we can easily check our units:
const is = type => any =>
(any !== null) &&
(typeof any === 'function' || typeof any === 'object') &&
('kind' in any) &&
(type === undefined || any.kind === type)
export const unit = is()
export const event = is('event')
export const store = is('store')
restore
restore
behaves differently on different inputs, so we will need our brand new is
functionality:
export const restore = (unit, defaultState) => {
if (is.store(unit)) {
return unit
}
if (is.event(unit)) {
return createStore(defaultState).on(unit, (_, x) => x)
}
const result = {}
for (const key in unit) {
result[key] = createStore(unit[key])
}
return result
}
restore
function can also handle effects, but we don't have them yet.
Other API functions, like sample
, guard
and combine
, we will describe in later chapters.
And as always, you can find all this chapter changes in this commit.
Thank you for reading!
To be continued...
Posted on April 4, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.