Skip to main content

8 posts tagged with "release"

View All Tags

ยท 10 min read
Marius Andra

Introducing Kea v3โ€‹

Since its origins in 2016 (it's been six years???), Kea has been on a mission to simplify frontend development, keeping pace, and adapting with the technological winds of change, as needed.

Since the last big rewrite in 2019, things have changed again. React 18 introduced Concurrent Mode. ECMAScript modules are in the browser. TypeScript is in all the things.

Kea's syntax hasn't kept up with the way Kea was being used, passing everything through a huge object keeps getting in the way of extensibility, and it's time for a refresh. It's 2022 after all.

Everything old will keep working, but here's the new:

// Kea 3.0
import { kea, actions, reducers, listeners, useActions } from 'kea'
import { loaders } from 'kea-loaders'
import { githubLogicType } from './githubLogicType'

export const githubLogic = kea<githubLogicType>([
actions({
setUsername: (username: string) => ({ username }),
}),
reducers({
username: ['keajs', { setUsername: (_, { username }) => username }],
}),
loaders({
repositories: [null, { setUsername: ({ username }) => api.getRepos(username) }],
}),
])

export function Github(): JSX.Element {
const { username, repositories } = useValues(githubLogic)
const { setUsername } = useActions(githubLogic)
return (
<>
<input value={userName} onChange={(e) => setUsername(e.target.value)} />
<div>repos: {repositories.map((r) => r.name).join(', ')}</div>
</>
)
}

Can you spot the difference?

// Kea 2.x
import { kea } from 'kea'
import { githubLogicType } from './githubLogicType'

export const githubLogic = kea<githubLogicType>({
actions: {
setUsername: (username: string) => ({ username }),
},
reducers: {
username: ['keajs', { setUsername: (_, { username }) => username }],
},
loaders: {
repositories: [null, { setUsername: ({ username }) => api.getRepos(username) }],
},
})

export function Github(): JSX.Element {
const { username, repositories } = useValues(githubLogic)
const { setUsername } = useActions(githubLogic)
return (
<>
<input value={userName} onChange={(e) => setUsername(e.target.value)} />
<div>repos: {repositories.map((r) => r.name).join(', ')}</div>
</>
)
}

The old "it is not legacy" 2.0 syntax is guaranteed to be supported until at least January 19th, 2038.

The new "it already feels more solid" 3.0 syntax is called "Logic Builders", and it brings a few surprising benefits.

You pass kea an array of LogicBuilders:

import { kea, actions, reducers, listeners, useActions } from 'kea'
import { loaders } from 'kea-loaders'

const logic = kea([
// put the `LogicBuilder`-s here ๐Ÿ‘
actions({}),
reducers({}),
loaders({}),
])

And get a logic in return.

But why are the logic builders in an array, and why is this syntax better than the old one?

Let's explore.

Logic Buildersโ€‹

Each logic builder is nothing more than a function that modifies the logic.

function actions(input) {
return (logic) => {
// do something to `logic`, based on `input`
}
}

Here's a peek inside the core actions builder to show how un-magical it all is:

function actions<L, I>(input: I): LogicBuilder<L> {
return (logic) => {
for (const [key, payload] of input) {
logic.actionsCreators[key] = createAction(key, payload)
logic.actions[key] = (...args: any[]) => dispatch(logic.actionsCreators[key](...args))
// etc...
}
}
}

The core logic builders are: actions, defaults, events, listeners, reducers, selectors.

While putting logic builders in an array to create logic is great fun, their real power comes from the realisation that logic builders can call other logic builders! ๐Ÿ’ก

With this insight, you can build all sorts of clever and highly practical abstractions, like loaders and forms:

const logic = kea([
forms({
loginForm: {
defaults: { user: '', pass: '' },
errors: ({ user, pass }) => ({
user: !user ? 'Please enter a user' : '',
pass: !pass ? 'Please enter a password' : '',
}),
submit: ({ user, pass }) => {
authLogic.actions.initLogin(user, pass)
}
},
})
])

export function forms<L extends Logic = Logic>(
input: FormDefinitions<L> | ((logic: BuiltLogic<L>) => FormDefinitions<L>),
): LogicBuilder<L> {
return (logic) => {
const forms = typeof input === 'function' ? input(logic) : input
for (const [formKey, formObject] of Object.entries(forms)) {
const capitalizedFormKey = capitalizeFirstLetter(formKey)

actions({
[`set${capitalizedFormKey}Value`]: (name: FieldName, value: any) => ({ name, value }),
[`reset${capitalizedFormKey}`]: (values?: Record<string, any>) => ({ values }),
[`submit${capitalizedFormKey}`]: true,
[`submit${capitalizedFormKey}Success`]: (formValues: Record<string, any>) => ({ [formKey]: formValues }),
[`submit${capitalizedFormKey}Failure`]: (error: Error) => ({ error }),
})(logic)

if (formObject.defaults) {
defaults({
[formKey]: formObject.defaults,
})(logic)
}

reducers({
[formKey]: {
[`set${capitalizedFormKey}Value`]: (
state: Record<string, any>,
{ name, value }: { name: FieldName; value: any },
) => deepAssign(state, name, value),
[`reset${capitalizedFormKey}`]: (state: Record<string, any>, { values }: { values: Record<string, any> }) =>
values || formObject.defaults || {},
},
// and so on

To learn more, read through the completely revamped documentation, starting with "What Is Kea?"

Logic Builder Codemodโ€‹

To automatically convert all logic into the new syntax, run:

npx kea-typegen@next write --convert-to-builders

New Featuresโ€‹

note

New to Kea? Start by reading the What is Kea page. The rest of this blog posts lists the differences between v2 and v3.

The official kea-forms pluginโ€‹

As hinted earlier, there's a new plugin that makes web forms spark joy again: kea-forms

import { kea } from 'kea'
import { forms, Form, Field } from 'kea-forms'
const loginLogic = kea([
forms({
loginForm: {
defaults: { user: '', pass: '' },
errors: ({ user, pass }) => ({
user: !user ? 'Please enter a user' : '',
pass: !pass ? 'Please enter a password' : '',
}),
submit: ({ user, pass }) => {
authLogic.actions.initLogin(user, pass)
},
},
}),
])

export function LoginForm(): JSX.Element {
return (
<Form logic={loginLogic} formKey="loginForm" enableFormOnSubmit>
{/* `value` and `onChange` are passed automatically to children of <Field> */}
<Field name="user">
<input type="text" />
</Field>
<Field name="pass">
<input type="password" />
</Field>
<button type="submit">Login!</button>
</Form>
)
}

Explicit afterMount and beforeUnmount buildersโ€‹

While events({ afterMount: () => {} }) works like before, you can now use afterMount and beforeUnmount directly.

Here's a logic that flips a message once per second, for as long as it's mounted:

import { actions, afterMount, beforeUnmount, kea, reducers } from 'kea'

const pingPongLogic = kea([
// create a simple counter
actions({ increment: true }),
reducers({ counter: [0, { increment: (state) => state + 1 }] }),
selectors({ message: [(s) => [s.counter], (counter) => (counter % 2 ? 'ping' : 'pong')] }),

// make it dance
afterMount(({ actions, cache }) => {
cache.interval = window.setInterval(actions.increment, 1000)
}),
beforeUnmount(({ cache }) => {
window.clearInterval(cache.interval)
}),
])

New propsChanged eventโ€‹

Instead of hacky useEffect loops, there's a new way to sync props from React to kea: the propsChanged event, which fires whenever React calls a logic with a new set of props.

Here's an over-engineered textfield that's controlled directly through props.

import React from 'react'
import {
kea,
actions,
reducers,
listeners,
props,
propsChanged,
path,
useValues,
useActions,
} from 'kea'
import type { textFieldLogicType } from './TextFieldType'

interface TextFieldProps {
value: string
onChange?: (value: string) => void
}

const textFieldLogic = kea<textFieldLogicType<TextFieldProps>>([
props({ value: '', onChange: undefined } as TextFieldProps),

actions({ setValue: (value: string) => ({ value }) }),
reducers(({ props }) => ({ value: [props.value, { setValue: (_, { value }) => value }] })),
listeners(({ props }) => ({ setValue: ({ value }) => props.onChange?.(value) })),

propsChanged(({ actions, props }, oldProps) => {
if (props.value !== oldProps.value) {
actions.setValue(props.value)
}
}),
])

export function TextField(props: TextFieldProps) {
const { value } = useValues(textFieldLogic(props))
const { setValue } = useActions(textFieldLogic(props))

return <input value={value} onChange={(e) => setValue(e.target.value)} />
}

New subscriptions pluginโ€‹

When listeners listen to actions, subscriptions listen to values. You can now run code when a value changes, no matter where the change originated from:

import { kea, actions, reducers } from 'kea'
import { subscriptions } from 'kea-subscriptions'

const logic = kea([
actions({ setMyValue: (value) => ({ value }) }),
reducers({ myValue: ['default', { setMyValue: (_, { value }) => value }] }),
subscriptions({ myValue: (value, oldValue) => console.log({ value, oldValue }) }),
])

logic.mount()
// [console.log] { value: 'default', oldValue: undefined }
logic.actions.setMyValue('coffee')
// [console.log] { value: 'coffee', oldValue: 'default' }
logic.actions.setMyValue('bagels')
// [console.log] { value: 'bagels', oldValue: 'coffee' }

useSelectorโ€‹

There's a new useSelector hook that works just like the one from react-redux

import { useSelector } from 'kea'

function Component() {
const value = useSelector((state) => state.get.my.value)
return <em>{value}</em>
}

Breaking changesโ€‹

No more peer dependenciesโ€‹

Feel free to remove redux, react-redux and reselect from your dependencies, unless you're using them directly.

Kea 3.0 removes react-redux (replaced via React 18's useSynExternalStore and its shim for older versions), and includes its dependencies redux and reselect directly. The kea package, and the various plugins, are all you need.

No more <Provider />โ€‹

It's no longer necessary to wrap your app in a <Provider /> tag.

If you're using react-redux's useSelector, switch to Kea's useSelector that doesn't need to be inside a <Provider />.

If you still need the tag for interoperability with non-kea Redux code, use react-redux's Provider with <Provider store={getContext().store}>.

Auto-Connect inside listeners is going awayโ€‹

Kea v2.0 introduced auto-connect, which was mostly a good idea. There's one place it didn't work:

const logic = kea({
listeners: {
getBread: () => {
shopLogic.actions.knockDoor()
},
},
})

Starting with Kea 2.0+, if shopLogic was not explicitly mounted, it would get mounted when accessed from within a listener.

It was a great idea, but came with subtle bugs due to the async nature of JS, and it's going away.

To safely migrate, upgrade to Kea 2.6.0, which will warn about automatically mounted logic. Fix all the notices, and make sure all logic is explicitly connected via connect, manually mounted, or mounted through a React hook.

You can also call something like userLogic.findMounted(), to access a logic only if it's mounted.

autoMount: true is also going awayโ€‹

There's was also an option to automatically mount a logic as soon as it was created. That's going away as well. If you still need this, make a plugin with an afterLogic hook.

Props mergeโ€‹

In earlier versions, the last used props overwrote whatever was there. Now props always merge:

const logic = kea([key(({ id }) => id)])
logic({ id: 1, value: 'blup' })
logic({ id: 1, other: true }).props === { id: 1, value: 'blup', other: true }

No more constantsโ€‹

Instead of constants from kea v2, use TypeScript Enums.

No more PropTypesโ€‹

All support for prop-types is dropped. You can no longer pass them to reducers or selectors.

After 6 long years, it's time to bid farewell to this relic of the early days of React.

Removed old connectโ€‹

Now that we have builders, connect is the name of an exported builder.

The previous connect, which was literally defined as:

const connect = (input) => kea({ connect: input })

... is gone. Use the snipped above if you need it.

The old connect was useful in the Kea v0 days, when React components were classes, and you used old decorators to connect actions and props values to components.

Those days are gone, and so is the old connect.

Remove props from connectโ€‹

The values key in connect({ actions: [], values: [] }) used to be called props. This was renamed to values and deprecated with Kea 1.0. Now it's gone.

Remove custom static payloadโ€‹

With Kea 3.0, an action can either be built with true (no payload) or a payload creator:

kea([
actions({
reset: true,
increment: (amount) => ({ amount }),
}),
])

Earlier versions allowed anything instead of true, and used that as the payload. If you still need that, just convert it into a function.

ยท 2 min read
Marius Andra

Kea 2.6 will be the last 2.x of Kea. Version 3.0 is just around the corner with big changes, and version 2.6 is here to provide a smoother upgrade path.

  • Update version requirements of peer dependencies: reselect 4.1+, redux 4.2+, react-redux 7+ and react 16.8+. If you're using React 18, upgrade react-redux to version 8+.

  • React 18 support: Add a "redux listener silencing store enhancer", which prevents Redux's useSelectors from updating, when mounting a logic from within the body of a React component (e.g. with useValues). This effectively silences log spam in React 18 (Warning: Cannot update a component (Y) while rendering a different component (X). To locate the bad setState() call inside X, follow the stack trace as described.), and improves performance.

  • Support custom selector memoization. Use memoizeOptions as the 4th selector array value, which is then passed directly to reselect:

const logic = kea({
selectors: {
widgetKeys: [
(selectors) => [selectors.widgets],
(widgets) => Object.keys(widgets),
null, // PropTypes, will be removed in Kea 3.0
{ resultEqualityCheck: deepEqual },
],
},
})
  • Set the autoConnectMountWarning option to true by default. Kea 2.0 introduced "auto-connect", and while it works great in reducers and selectors, automatically connecting logic in listeners turned out to be a bad idea. Thus, in Kea 2.6, when accessing values on an unmounted logic, you'll get a warning by default. In Kea 3.0, it will trigger an error.
import { kea } from 'kea'
import { otherLogic } from './otherLogic'
import { yetAnotherLogic } from './yetAnotherLogic'

const logic = kea({
// connect: [otherLogic], // should have been explicitly connected like this, or mounted outside the logic
actions: { doSomething: true },
listeners: {
doSomething: () => {
// This will now print a warning if `otherLogic` is not mounted.
// Either add it to "connect" or make sure it's mounted elsewhere.
console.log(otherLogic.values.situation)
},
},
reducers: {
something: [
null,
{
// This `yetAnotherLogic` will still get connected automatically, not print a warning,
// and not require `connect`. That's because it's connected directly at build time, whereas
// in the listener, we're running within an asynchronous callback coming from who knows where.
// While this works, it's still good practice to explicitly define your dependencies.
[yetAnotherLogic.actionTypes.loadSessions]: () => 'yes',
},
],
},
})

ยท One min read
Marius Andra

What's that? A new release?!?

Kea 2.4 brings the following:

  • Fixed crashes with React Fast Refresh.

  • Changed the default path for logic without a path (or when not using the kea babel plugin) from kea.inline.2 to kea.logic.2. If you have ever hardcoded "kea.inline" anywhere, perhaps in tests, this will cause a bit of headache. If you need it set at kea.inline, use: resetContext({ defaultPath: ['kea', 'inline'] }).

  • Added <Provider /> tag to simplify calling React-Redux's <Provider store={getContext().store} />.

Oh, and there's a new Kea CRA template you can use.

Starting a new CRA project with all the Kea tools configured is now as simple as running:

yarn create react-app my-kea-app --template kea-typescript
cd my-kea-app
yarn start

... and you're good to go!

ยท One min read
Marius Andra

TypeScript support in Kea just got a boost! Starting with kea-typegen 0.7.0 we also:

  • Automatically add import { logicType } from './logicType' in logic files.
  • Automatically add the logicType type to kea<logicType>().
  • Separate changed files in the log with ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ!
Auto Import Logic Type

ยท 2 min read
Marius Andra
TL;DR

New tag: <BindLogic logic={itemLogic} props={{ id: 12 }}>. It lets you write useValues(itemLogic) instead of useValues(itemLogic({ id: 12 })) in nested children. Powered by React Context.

When using keyed logics it's easy to get into a situation where you need to pass props similar to this:

import { kea, useValues } from 'kea'

const itemLogic = kea({ key: (props) => props.id /* ... */ })

function List() {
return (
<>
<Item id={12} />
<Item id={15} />
</>
)
}

function Item({ id }) {
const { className } = useValues(itemLogic({ id }))

return (
<div className={className}>
<Title id={id} />
<Body id={id} />
</div>
)
}

function Title({ id }) {
const { title, url } = useValues(itemLogic({ id }))
return <a href={url}>{title}</a>
}

function Body({ id }) {
const { body } = useValues(itemLogic({ id }))
return <div>{body}</div>
}

In Kea 2.3 you can use <BindLogic> to clear this up:

import { kea, useValues, BindLogic } from 'kea'

const itemLogic = kea({ key: (props) => props.id /* ... */ })

function List() {
return (
<>
<BindLogic logic={itemLogic} props={{ id: 12 }}>
<Item />
</BindLogic>
<BindLogic logic={itemLogic} props={{ id: 15 }}>
<Item />
</BindLogic>
</>
)
}

function Item() {
const { className } = useValues(itemLogic)

return (
<div className={className}>
<Title />
<Body />
</div>
)
}

function Title() {
const { title, url } = useValues(itemLogic)
return <a href={url}>{title}</a>
}

function Body() {
const { body } = useValues(itemLogic)
return <div>{body}</div>
}

When you write <BindLogic logic={itemLogic} props={{ id: 12 }}>, Kea stores a specific build of itemLogic inside a React Context. Any nested children will get that when calling e.g: useValues(itemLogic).

ยท 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.

ยท 10 min read
Marius Andra
note

New to Kea or saw it last a few years ago? You should take a closer look. A lot has changed and you might like what you see! ๐Ÿ˜ƒ

8 months after the release of Kea 1.0 I'm proud to announce version 2.0!

This version brings several convenience features. It's a rather small release, yet there were a few breaking changes, which warranted a new major version.

What changed? Read below!

But first! You are reading this blog post in the brand new documentation for Kea! Powered by docusaurus v2! Over 17000 new words were written for these docs in an effort to really clarify how Kea works.

Start with What is Kea? if you're new here. Then head on to the core concepts. Please also read them if you've been using Kea for a while. You might learn something you didn't know! Then check out additional concepts, debugging and other pages for more.

Anyway, where were we?

Oh yes, new stuff in Kea 2.0! ๐Ÿคฉ

Listeners built in (1 Breaking Change)โ€‹

For years Kea has supported two different side effect libraries: sagas and thunks.

With Kea 1.0, I added a new lightweight one called listeners.

Listeners solve the main issue with thunks (you can't use thunks in reducers) and let you write much simpler code than sagas, while retaining the most commonly used features of sagas (debouncing and cancelling workers a'la takeLatest). Unless you're writing highly interactive applications, you will probably not need to use sagas anymore.

Before 2.0 listeners was an optional plugin, but now it's included by default. This enables two big things:

  • Much easier to get started with Kea
  • Plugin authors have a side-effect library that they can always rely on instead of writing bindings for 3 different systems.

Weighing at just 1.4KB (gzipped, 3.4KG minified), including listeners in kea doesn't add a lot of weight.

Yet if you wish to disable them, use skipPlugins when upgrading:

resetContext({ skipPlugins: ['listeners'] })

Breaking change, please note: If you were using listeners with Kea 1.0, make sure to remove listenersPlugin from your resetContext({ plugins: [] }) array or Kea will complain that it's being imported twice.

Writing [actions. and ] is now optionalโ€‹

This used to be the only way to write reducers and listeners:

// Works in all versions of Kea
const logic = kea({
actions: () => ({
increment: (amount) => ({ amount }),
setCounter: (counter) => ({ counter }),
reset: true
}),
reducers: ({ actions }) => ({
counter: [0, {
[actions.increment]: (state, { amount }) => state + amount,
[actions.setCounter]: (_, { counter }) => counter,
[actions.reset]: () => 0
}]
}),
listeners: ({ actions }) => ({
[actions.reset]: () => {
console.log('reset called')
}
})
})

Now you can do this:

// Works with Kea 2.0+
const logic = kea({
actions: () => ({
increment: (amount) => ({ amount }),
setCounter: (counter) => ({ counter }),
reset: true
}),
reducers: () => ({
counter: [0, {
increment: (state, { amount }) => state + amount,
setCounter: (_, { counter }) => counter,
reset: () => 0
}]
}),
listeners: () => ({
reset: () => {
console.log('reset called')
}
})
})

If your actions are defined in the same logic (or imported with connect), you can skip writing [actions. ] and also skip ({ actions }).

Writing [actions.increment] will still work, just like writing [otherLogic.actions.actionName].

This will be especially nice for TypeScript users, who were forced to write [actions.increment as any] to avoid constantly bumping into "error TS2464: A computed property name must be of type 'string', 'number', 'symbol', or 'any'".

Auto-Connect!โ€‹

Up to Kea 1.0, when you used actions or values from otherLogic inside your logic, you had to connect them together.

import { counterLogic } from './counterLogic'

// Works in all versions of Kea
const logic = kea({
connect: {
// pulling in actions from `counterLogic`
actions: [counterLogic, ['increment', 'decrement']],
// pull in values from `counterLogic`
values: [counterLogic, ['counter']],
},

listeners: ({ actions, values }) => ({
[actions.increment]: () => {
console.log('Increment called!')
console.log(`Counter: ${values.counter}`)
}
})
})

Now you can skip connect (if you want to) and call all actions and values directly on counterLogic:

import { counterLogic } from './counterLogic'

// Works in Kea 2.0+
const logic = kea({
listeners: () => ({
[counterLogic.actions.increment]: () => {
console.log('Increment called!')
console.log(`Counter: ${counterLogic.values.counter}`)
}
})
})

While this also works in Kea 1.0 under some conditions, the code above will always work with Kea 2.0.

In version 1.0 you had to manually assure that counterLogic was mounted before calling actions and values on it. Perhaps it was mounted via useValues in React or alternatively you could also write: connect: { logic: [counterLogic] } without specifying what exactly to connect. The code above would then also work.

In version 2.0 this is no longer necessary. When you:

  • use counterLogic.actions.increment as a key in reducers or listeners
  • use counterLogic.selectors.counter in selectors
  • use counterLogic.anything.really inside a listener

... then counterLogic is automatically connected to logic and mounted/unmounted when needed.

This means the following code will also work:

import { counterLogic } from './counterLogic'

// Works in Kea 2.0+
const logic = kea({
actions: () => ({
showCount: true
}),
listeners: () => ({
showCount: () => {
console.log('Increment called!')
console.log(`Counter: ${counterLogic.values.counter}`)
}
})
})

In this example, the first time you use counterLogic is inside a listener when getting a value from it.

If counterLogic was not already mounted, it will be mounted directly when you call showCount. It will stay mounted for as long as logic is still mounted. It will be unmounted together with logic in case no other mounted logic or component has a lock on it.

There is one caveat with autoConnect for when you want to manually call mount() and unmount() inside a listener. For that please read the section in the Using without React page.

To opt out of autoConnect, pass autoConnect: false to resetContext.

(Optional) Babel plugin to autogenerate pathsโ€‹

If you have ever used the redux devtools, to debug your logic, you will have noticed that unless you specify a path in your logic, it will be automatically placed under kea.inline.[N] like so:

Redux Devtools with Inline Paths

With the new babel-plugin-kea, these paths can be autogenerated from the filesystem, greatly enhancing your debugging experience:

Redux Devtools with Autogenerated Paths

What's more, this can be used in combination with plugins like kea-localstorage or in frameworks like next.js to persist values or hydrate server-rendered logic easier than ever before.

Other smaller improvementsโ€‹

Those were the big ones. A few other things made it into Kea 2.0.

You can extend reducersโ€‹

Previously in this case:

// Works with Kea 1.0
const logic = kea({
actions: () => ({
doSomething: true,
doSomethingMore: true,
}),
reducers: ({ actions }) => ({
myValue: [0, {
[actions.doSomething]: () => 100
}]
})
})
logic.extend({
reducers: ({ actions }) => ({
myValue: [0, {
[actions.doSomethingMore]: () => 10000
}]
})
})

The entire reducer for myValue would be overridden. This means only the action doSomethingMore would have any effect on the value. This is no longer the case and the reducer mapping is merged when a reducer is extended.

In case of conflicts, later actions override previously defined ones. However the first default value is taken. To override a default, just specify it separately with defaults: { myValue: 100 } within kea({})

In resetContext, createStore is now true by defaultโ€‹

Previously when using resetContext and not using any other redux-specific middleware or libraries, you had to write:

// Works with all versions of Kea, but not needed in 2.0
resetContext({
createStore: true // or {}
})

Omitting this createStore: true line would cause Kea to fail. This is no longer necessary. The redux store will be created when you call resetContext without any arguments. Pass false to createStore if you wish to avoid this.

The path in your logic can start with anythingโ€‹

Previously you had to write:

// Works with all versions of Kea, but not needed in 2.0
resetContext({
createStore: {
// defaulted to ['kea', 'scenes']
paths: ['kea', 'scenes', 'pages', 'components']
}
})

... if you wanted your logic.path to start with pages or anything other than kea or scenes. The first part of the path had to be whitelisted.

This is no longer necessary. If you omit paths in createStore, you can use whatever string for the first part of your logic's path.

Specifying paths reverts to whitelisting and anything else is disallowed. Only now it will also throw an error instead of silently just not connecting the logic to redux.

Create a reducer without a defaultโ€‹

This used to be the only way to define reducers:

const counterLogic = kea({
actions: () => ({
increment: true,
decrement: true,
}),

reducers: ({ actions }) => ({
counter: [0, { // `0` as default
[actions.increment]: (state) => state + 1,
[actions.decrement]: (state) => state - 1
}]
})
})

Now if you prefer, you can omit the default value in reducers:

const counterLogic = kea({
actions: () => ({
increment: true,
decrement: true,
}),

reducers: () => ({
counter: { // `null` as default if not given in `defaults`
increment: (state) => (state || 0) + 1,
decrement: (state) => (state || 0) - 1
}
})
})

... and either define it in defaults or not at all. It'll just be null if not defined.

Action type string no longer skips scenes.โ€‹

This is a very minor tweak.

Previously if your action had a path that started with scenes, then it was skipped in the action type toString().

// before
homepageLogic.path == ['scenes', 'homepage', 'index']
homepageLogic.action.reloadPage.toString() === 'reload page (homepage.index)'

accountLogic.path == ['menu', 'account', 'index']
accountLogic.action.reloadAccount.toString() === 'reload account (menu.account.index)'

Now it's included:

// after
homepageLogic.path == ['scenes', 'homepage', 'index']
homepageLogic.action.reloadPage.toString() === 'reload page (scenes.homepage.index)'

accountLogic.path == ['menu', 'account', 'index']
accountLogic.action.reloadAccount.toString() === 'reload account (menu.account.index)'

I told you this was a very minor tweak!

That's it for new stuff in Kea 2.0. Please let me know what is your favourite new feature or if you have anything else to share! ๐Ÿ‘‹

What's next? (Kea 2.1 and/or 3.0)โ€‹

There are two main things I'd like to explore in the next versions of Kea.

TypeScript supportโ€‹

One of the most requested features for Kea has been proper TypeScript support. While you can get pretty far with Kea in TS if you manually create your interfaces, this is sub-optimal.

The goal for Kea 2.1 (or 3.0?) is to have full and automatic TypeScript support. In fact, many of the changes with 2.0 (namely eliminating the need for connect & no need to write [actions.__]) were done to pave the way.

Even if you don't use TypeScript, this will help IDEs offer proper autocomplete support when writing Kea in regular JavaScript.

Precomplicationโ€‹

At the end of the day, Kea is just an engine that converts input into logic plus a framework to mount/unmount this logic when requested by React components.

What if we could do some of this conversion at compile-time, rather than at runtime?

Now that we have a babel plugin that automatically adds paths to logic, could this be extended to speed up runtime Kea by inlining some of these conversions where possible? Would it make a difference in runtime speed?

Kea's performance has never been an issue so far, but this is an interesting avenue for some exploration.

To be continued.