Kea 2.1 does two things:
- Continues the "let's make things simpler" trend started in Kea 2.0
by removing another bunch of squiggly bits that you will not need to type again:
" ((((((()))))))===>>>{}"
- Adds support for accessing the state before an action was fired inside listeners.
It's also backwards compatible: Logic written for Kea version 1.0 will still run in 2.1.
The saga until now:โ
This is Kea 1.0:
const logic = kea({
actions: () => ({
goUp: true,
goDown: true,
setFloor: floor => ({ floor })
}),
reducers: ({ actions }) => ({
floor: [1, {
[actions.goUp]: state => state + 1,
[actions.goDown]: state => state - 1,
[actions.setFloor]: (_, { floor }) => floor
}]
}),
selectors: ({ selectors }) => ({
systemState: [
() => [selectors.floor],
floor => floor < 1 || floor > 20 ? 'broken' : 'working'
]
}),
listeners: ({ actions, values }) => ({
[actions.setFloor]: ({ floor }) => {
console.log('set floor to:', floor)
if (values.systemState === 'broken') {
console.log('you broke the system!')
}
}
})
})
In Kea 2.0 we can skip [actions.]
and { actions }
:
const logic = kea({
actions: () => ({
goUp: true,
goDown: true,
setFloor: floor => ({ floor })
}),
reducers: () => ({ // removed "{ actions }"
floor: [1, {
goUp: state => state + 1, // removed "[actions.]"
goDown: state => state - 1, // removed "[actions.]"
setFloor: (_, { floor }) => floor // removed "[actions.]"
}]
}),
selectors: ({ selectors }) => ({
systemState: [
() => [selectors.floor],
floor => floor < 1 || floor > 20 ? 'broken' : 'working'
]
}),
listeners: ({ values }) => ({
setFloor: ({ floor }) => { // changed
console.log('set floor to:', floor)
if (values.systemState === 'broken') {
console.log('you broke the system!')
}
}
})
})
You can still write [actions.]
explicitly... and you do it mostly when
using actions from another logic:
import { janitorLogic } from './janitorLogic'
const elevatorLogic = kea({
reducers: ({ actions }) => ({
floor: [1, {
goUp: state => state + 1, // local action
[actions.goDown]: state => state - 1, // no longer useful
[janitorLogic.actions.setFloor]: (_, { floor }) => floor
}]
}),
})
... but you save 41 keystrokes in the default case:
"{ actions }[actions.][actions.][actions.]" // byebye
Changed in Kea 2.1:โ
Why stop there?
There's another low hanging fruit we can eliminate: () => ({})
.
Gone!
const logic = kea({
actions: { // removed "() => ("
goUp: true,
goDown: true,
setFloor: floor => ({ floor })
}, // removed ")"
reducers: { // removed "() => ("
floor: [1, {
goUp: state => state + 1,
goDown: state => state - 1,
setFloor: (_, { floor }) => floor
}]
}, // removed ")"
selectors: ({ selectors }) => ({
systemState: [
() => [selectors.floor],
floor => floor < 1 || floor > 20 ? 'broken' : 'working'
]
}),
listeners: ({ values }) => ({
setFloor: ({ floor }) => {
console.log('set floor to:', floor)
if (values.systemState === 'broken') {
console.log('you broke the system!')
}
}
})
})
16 units of squiggly bits gone! Here they are, in chronological and ascending order:
"() => ()() => ()" // chronological
" (((())))==>>" // ascending
They are there if you need them, of course. For example when using
props
in reducers
:
kea({
reducers: ({ props }) => ({
floor: [props.defaultFloor, {
goUp: state => state + 1,
goDown: state => state - 1,
}]
}),
})
What about the selectors? How can we simplify this?
kea({
selectors: ({ selectors }) => ({
systemState: [
() => [selectors.floor],
floor => floor < 1 || floor > 20 ? 'broken' : 'working'
]
})
})
Here's the simplest backwards-compatible change that went into Kea 2.1:
kea({
selectors: { // spot
systemState: [
selectors => [selectors.floor], // the
floor => floor < 1 || floor > 20 ? 'broken' : 'working'
]
} // difference
})
Goodbye another 14 spaces and squgglies:
"({ }) => ()()"
If you're really feeling the minimalist vibe, you could also simplify
the object in listeners
and events
, but:
const elevatorLogic = kea({
listeners: {
setFloor: ({ floor }) => {
console.log('set floor to:', floor)
if (elevatorLogic.values.systemState === 'broken') {
console.log('you broke the system!')
}
}
}
})
You might get tired of writing thisLogic
everywhere.
In general, the suggestion is to always write the simplest thing first:
kea({
reducers: {
poteito: // ...
}
})
... and only when needed, extend it into a function to pull in objects and evaluate lazily:
kea({
reducers: ({ props }) => ({
potaato: // ...
})
})
Previous State in Listenersโ
There's a certain way listeners work:
kea({
actions: {
setFloor: floor => ({ floor })
},
reducers: {
floor: {
setFloor: (_, { floor }) => floor
}
},
listeners: ({ values }) => ({
setFloor: ({ floor }, breakpoint, action) => {
// { floor } = payload of the action
// breakpoint = some cool stuff ;)
// action = the full redux action, in case you need it
console.log("floor in action payload: ", floor)
console.log("floor in state: ", values.floor)
if (floor === values.floor) { // this is true
console.log('the reducer already ran')
console.log('before the listener started')
}
}
}),
)
The action
will first update the reducers
and only then run the listener
.
What if you really need the state before the action ran?
You could set up a two step system (setFloorStart
& setFloorUpdate
)
... or you could use previousState
, the new 4th argument to listeners:
kea({
actions: {
setFloor: floor => ({ floor })
},
reducers: {
floor: {
setFloor: (_, { floor }) => floor
}
},
listeners: ({ selectors, values }) => ({
setFloor: ({ floor }, _, __, previousState) => {
// { floor } = payload
// _ = breakpoint
// __ = action
// previousState = the state of the store before the action
const lastFloor = selectors.floor(previousState)
if (floor < lastFloor) {
console.log('going down!')
}
if (floor > lastFloor) {
console.log('going up!')
}
}
}),
)
Take the store's previousState
(new 4th argument) and run it through any
selector
you can get your hands on. Every value
has a selector
, so you
have plenty to choose from.
How does this work?
This is just another benefit of using Redux under the hood. More specifically, using the idea redux popularised: store everything in one large tree and propagate changes in its branches through cascading immutable updates.
Every unique version of the entire state tree ends up in a plain JS object
.
This state object is only read from and it will never change... and it's discarded
as soon as the next state
comes in.
We can still keep a reference to this previous state and use selectors on it to get whichever selected or computed value we need.
Easy as pie!
Mmm... pie. ๐ฐ
4th argument?
Yeah, it's getting busy up there, but ๐คท. I'm not going to make a breaking change for this.