Skip to main content

kea

Logic builders

To build a logic, you give kea an array of logic builders.

Before a logic is mounted, it is built, and all logic builders are run in succession:

import { kea, actions, BuiltLogic } from 'kea'

const logic = kea([
// add `setUsername` action to logic
actions({
setUsername: (username) => ({ username }),
}),
// add `username` reducer, selector and value to logic
reducers({
username: ['keajs', { setUsername: (_, { username }) => username }],
}),

// do something custom
(logic: BuiltLogic) => {
logic.cache.foobar = logic.actions.setUsername
},

// a logic builder that calls another logic builder inside it
(logic: BuiltLogic) => {
actions({
setPassword: (password) => ({ password }),
})(logic)
},
])

A logic builder has the format (logic: BuiltLogic) => { /* anything */ }, and its job is to modify any of the properties of a logic.

Normally you don't manipulate properties of your logic directly, but you use wrap core builders like actions and listeners instead.

note

Technically, functions like actions and reducers are logic-builder-builders, since they return a LogicBuilder, but, to keep everyone's mental health expenses down, we're calling all functions that return logic builders, like actions or listeners, just "builders" for short.

You can build powerful abstractions when you nest builders. For example, here's a setters builder, which creates a thing reducer, and a corresponding setThing action, for every thing key it finds in its input:

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

const capitalize = (s: string) => `${s.charAt(0).toUpperCase()}${s.slice(1)}`

function setters(input: Record<string, any>) {
return (logic) => {
for (const [key, value] of Object.entries(input)) {
actions({ [`set${capitalize(key)}`]: (value) => ({ [key]: value }) })(logic)
reducers({ [key]: [value, { [`set${capitalize(key)}`]: (_, p) => p[key] }] })(logic)
}
}
}

const loginLogic = kea([
setters({ username: 'keajs', password: '' }), // much shorter
])
loginLogic.mount()
loginLogic.actions.setUsername('posthog')
loginLogic.values.username === 'posthog'
loginLogic.values.password === ''

I'd prefer using actions and reducers directly myself, but who knows, maybe setters can simplify your app.

If not, perhaps you will use the kea-forms plugin at some point. It's built exactly the same way: you pass forms() a few input parameters, and it'll add the relevant actions and values to your logic:

import { kea, forms } from 'kea'

const logic = kea([
forms({
signupForm: {
defaults: { username: '', password: '' },
errors: ({ password }) => ({
password: password.length < 8 ? 'password too short' : undefined,
}),
submit: ({ username, password }) => api.fetchSignupResponse(username, password),
},
}),
])

logic.mount()
logic.actions.setSignupFormValue('password', 'asd')
logic.values.signupFormErrors.password === 'password too short'

logic.actions.submitSingupForm()
logic.values.isSignupFormSubmitting === true

It's a very practical way to build frontend apps.

Input objects vs functions

Whenever you're using any of kea's built-in primitives (actions, reducers, listeners, etc), you have two options.

You can pass objects to them:

kea([
actions({
increment: true,
}),
listeners({
increment: () => {
console.log('incrementing!')
},
}),
])

... or you can pass functions to them, which take the same logic as their only input, and get evaluated only on build:

kea([
actions((logic) => ({
increment: true,
})),
listeners((logic) => ({
increment: () => {
console.log('incrementing!')
},
})),
])

You can get actions, values and other goodies from this logic:

kea([
listeners(({ actions, values }) => ({
increment: () => {
if (values.iHaveHadEnough) {
actions.doSomethingElse()
}
},
})),
])

The recommendation is to write the simplest code you can (start with reducers({})), and convert it into a function when you need any data from the logic.

There's one more case when converting the input to a function is useful. Sometimes, due to the order in which your browser loads modules, some imported values will be undefined when the code first runs. For example:

import { kea, connect } from 'kea'
import { otherLogic } from './otherLogic'

const logic = kea([
connect(otherLogic), // if this is `undefined` when this code is executed
connect(() => otherLogic), // wrap it in a function to load lazily
])

Kea 2.0 input object syntax

Kea 3.0 introduced logic builders. Before that, you had to pass an object to kea({}) with keys representing the builders:

const logic = kea({
actions: {
increment: (amount = 1) => ({ amount }),
decrement: (amount = 1) => ({ amount }),
},
reducers: {
counter: [
0,
{
increment: (state, { amount }) => state + amount,
decrement: (state, { amount }) => state - amount,
},
],
},
})

This syntax still works, and is guaranteed be supported until at least January 19th, 2038.

However, you're encouraged to use Kea 3.0's builder syntax. Mostly because it makes it a lot easier to use and build custom builders.

You can also mix and match, for example:

import { kea, actions, listeners } from 'kea'

const logic = kea([
{ actions: { doThing: true } },
actions({ doAnotherThing: true }),
{ reducers: {}, selectors: {} },
listeners({
doThing: () => {},
doAnotherThing: () => {},
}),
])

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

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

Questions & Answers

Ask questions about this page here.