Skip to main content

Kea 2.1: Less squggly bits ๐Ÿ› and previous state in listeners ๐Ÿฆœ๐Ÿฆœ

ยท 7 min read
Marius Andra

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.

Questions & Answers