https://keajs.org/blog/kea-3.0/ Skip to main content Kea LogoKea Logo Kea 3.0DocsBlog OpenCollectiveGitHub Recent posts * Kea 3.0: Logic Builders * Kea 2.6: Last 2.x release * Live-Replay Logic Testing * Kea-TypeGen 1.0: Auto Import * It's time for a data-first frontend revolution * Kea 2.4: Fast Refresh * Kea-TypeGen 0.7: Auto Imports * Kea 2.3: Bind Logic * Kea 2.2: The TypeScript Release * TypeScript Support in Kea (The Long Road) * Kea 2.1: Less squggly bits and previous state in listeners * New stuff: Gists, WaitFor, Testing * Kea 2.0 Released Kea 3.0: Logic Builders May 12, 2022 * 10 min read Marius Andra Marius Andra Kea Core Team 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([ 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 ( <> setUsername(e.target.value)} />
repos: {repositories.map((r) => r.name).join(', ')}
) } Can you spot the difference? // Kea 2.x import { kea } from 'kea' import { githubLogicType } from './githubLogicType' export const githubLogic = kea({ 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 ( <> setUsername(e.target.value)} />
repos: {repositories.map((r) => r.name).join(', ')}
) } 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(input: I): LogicBuilder { 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( input: FormDefinitions | ((logic: BuiltLogic) => FormDefinitions), ): LogicBuilder { 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) => ({ values }), [`submit${capitalizedFormKey}`]: true, [`submit${capitalizedFormKey}Success`]: (formValues: Record) => ({ [formKey]: formValues }), [`submit${capitalizedFormKey}Failure`]: (error: Error) => ({ error }), })(logic) if (formObject.defaults) { defaults({ [formKey]: formObject.defaults, })(logic) } reducers({ [formKey]: { [`set${capitalizedFormKey}Value`]: ( state: Record, { name, value }: { name: FieldName; value: any }, ) => deepAssign(state, name, value), [`reset${capitalizedFormKey}`]: (state: Record, { values }: { values: Record }) => 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 (
{/* `value` and `onChange` are passed automatically to children of */} ) } 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>([ 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 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 {value} } 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 It's no longer necessary to wrap your app in a tag. If you're using react-redux's useSelector, switch to Kea's useSelector that doesn't need to be inside a . If you still need the tag for interoperability with non-kea Redux code, use react-redux's Provider with . 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 [DEL:props:DEL] 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. Questions & Answers Tags: * kea * release Older Post Kea 2.6: Last 2.x release * Introducing Kea v3 * Logic Builders * Logic Builder Codemod * New Features + The official kea-forms plugin + Explicit afterMount and beforeUnmount builders + New propsChanged event + New subscriptions plugin + useSelector * Breaking changes + No more peer dependencies + No more + Auto-Connect inside listeners is going away + autoMount: true is also going away + Props merge + No more constants + No more PropTypes + Removed old connect + Remove props from connect + Remove custom static payload Copyright (c) 2015-2022 Marius Andra and other Kea contributors. Subscribe to new content