18 comments

[ 2.5 ms ] story [ 45.3 ms ] thread
I was happy there is some interesting improvement to kea DHCP server, but it's just another react library.
Same here. Kea DHCP desperately needs some love, it's barely better than the venerable ISC DHCP server which is a pain for a modern deployments (HA, API)
Has anyone reading this used Kea, especially in production, and what was your experience?
We use it in prod at PostHog (https://posthog.com/) and it's SUPER practical as an abstraction over Redux's boilerplate. Also makes for really sensible separation of logic from presentation. Only TypeScript support requires a bit of additional setup as type inference doesn't work with this architecture.
Just out of curiosity, have you seen our official Redux Toolkit package? We specifically designed it to eliminate the "boilerplate" concerns and work great with TS:

- https://redux.js.org/tutorials/essentials/part-2-app-structu...

- https://redux.js.org/introduction/why-rtk-is-redux-today

- https://redux.js.org/tutorials/typescript-quick-start

I'm also interested in hearing any particular features in Kea you find useful that aren't covered in RTK.

Well, in PostHog (I'm a colleague of Twixes), we just decided here [1] to go with Kea, as I had more experience with it.

Had Redux Toolkit been around when I started with Redux in 2016, I probably wouldn't have needed to write Kea. However I'm glad I did, as I now do consider Kea to be the better toolkit for the same job. :D. I know you disagree, and that's all fine of course :).

In my mind Kea fills in all the missing parts from React, while Redux Toolkit is still a set of utility functions for state management. You still need to find another set of tools for data loading (I see this has improved), complex data or form management, app routing, scene handling, etc. Redux Toolkit doesn't bother with most of that, and I don't think it should.

Kea however comes with solutions for all of those. The only thing missing from being able to build a complete app is a JS module bundler. Just sit down and start writing your actual app with the least amount of syntax possible to explain things in a redux'y way. Many people have told me that with Kea, the framework basically gets out of the way and disappears.

Now with the Kea 3.0 Logic Builder syntax, Kea is suddenly like lego for state management. I'm curious to see if this picks up or not. I can't unfortunately link Kea from redux.js.org to drive traffic :).

[1] https://github.com/PostHog/posthog/issues/301 -- the magic of working in an open source company is that we can post stuff like this. We're hiring!

Haha, nice, that does explain why PostHog adopted Kea! :)

I know I've said this in a couple issue threads, but just to re-state it publicly:

As Redux maintainer and RTK creator, I do feel RTK is the best approach for using Redux, and that's why we now teach it as default.

But it's great that you've been able to build out an alternate toolset on top of the Redux core that suits your needs and share that, and it's _also_ my job as maintainer to make sure that you still have the tools available to keep that working. (which is why even though I've marked `createStore` as deprecated per our prior issue threads, we're not going to _remove_ it - too many apps and libs depend on it as-is.)

And yeah, agreed that Kea and RTK have different areas of focus. We don't _want_ to handle routing or forms :)

that's far too considerate an opinion to put on the internet :)
A bit like introducing immutability removes bugs you don't understand... each time I see us replace some `useState` `useEffect` orchestration with a kea logic things get less buggy
To my eye, this architecture still forces too much needless boilerplate. Seems like it will work well for apps that have a low number of very important features. Apps that naturally have a lot of systems will end up paying a high overhead multiplier over time.
OP here, so call me biased, but I tend to disagree. Curious to hear your thought on alternative or better approaches to system design here.

In my experience you need some amount of boilerplate, or what I'd call _architecture_, to have a stable system. Give too much freedom ("just use get/set" or "variables on context"), and not only can no other developer on your team not maintain your code, but no part of your app will work together after a while. You'll end up with multiple incompatible implementations of ad-hoc state machines.

The key to maintainability is to use a framework that forces you to do spend a bit more effort than you'd do when coding YOLO, but which forces everyone on the team to make the same tradeoff. The result: everyone can jump in to any part of the project and with a bit of digging around be fluent in the surroundings.

Kea seems to strike a pretty good balance of making this possible in large projects.

Yes - having an architecture that ensures maintainability is critical. But good architecture needn't require this much boilerplate. There is a lot of non-linear control flow encoded implicitly in calls to `kea`, `actions`, `listeners` in this framework, and for what?

Here's one of the examples from the docs (https://keajs.org/docs/core/listeners/#error-handling)

    kea([
      actions({
        loadUsers: true,
        loadUsersSuccess: (users) => ({ users }),
        loadUsersFailure: (error) => ({ error }),
      }),
    
      reducers({
        users: [
          [],
          {
            loadUsersSuccess: (_, { users }) => users,
          },
        ],
        usersLoading: [
          false,
          {
            loadUsers: () => true,
            loadUsersSuccess: () => false,
            loadUsersFailure: () => false,
          },
        ],
        usersError: [
          null,
          {
            loadUsers: () => null,
            loadUsersFailure: (_, { error }) => error,
          },
        ],
      }),
    
      listeners(({ actions }) => ({
        loadUsers: async () => {
          try {
            const users = await api.get('users')
            actions.loadUsersSuccess(users)
          } catch (error) {
            actions.loadUsersFailure(error.message)
          }
        },
      })),
    ])
    
I argue that an API with a different shape could implement the same behavior in a more direct readable style, without giving up any of the nice debugging, etc that you get with Kea's abstractions.

    class Users extends OtherKeaAPI {
      users = this.state<User[]>([])
      usersLoading = this.state(false)
      usersError = this.state<Error | null>(null)
    
      loadUsers = this.action(action => {
        this.usersLoading = true
        this.usersError = null
     
        action.effect('load em up', async () => {
          try {
            this.loadUsersSuccess(await api.get('users'))
          } catch (error) {
            this.loadUsersFailure(error)
          }
        })
      })
    
      loadUsersSuccess = this.action((users: User[]) => {
        this.usersLoading = false
        this.users = users
      })
    
      loadUsersFailure = this.action((error: Error) => {
        this.usersLoading = false
        this.usersError = error
      })
    }
Depends what you mean with "readable". It's always easy to make a contrived example, and call that readable because it's shorter. I'm talking about huge apps maintained by multiple remote teams, all working independently.

In this case I would argue that the end goal is "maintainability", not "readability", and Kea, thanks to forcing you to use an abstraction in the first place, leads to better maintainable code.

Piping all "writes" through actions, and all "reads" through selectors gives you complete observability over the entire system at any point in time. The immutable nature of this approach also works wonders in preventing React from wasting time with needless re-renders. Plus the syntax makes it that you can jump into any part of a system consisting of 200+ logic files, and immediately understand how each piece of data came about, and what are the only things that can change it.

Using "simple" and "readable" plain functions that go `this.something = "value"` is the antithesis of a maintainable system. You lose all observability, unless you integrate a lot of "magic", for example making every value secretly use "immer".

> There is a lot of non-linear control flow encoded implicitly in calls to `kea`, `actions`, `listeners` in this framework, and for what?

To build large applications that scale well, are easy to reason about, and can be maintained by whoever joins your team.

> You lose all observability, unless you integrate a lot of "magic", for example making every value secretly use "immer".

Oh - of course. But I think Kea’s current syntax is also a kind of magic; but a different sort of magic making different tradeoffs. For example - the typescript typedef generation build step magic is a consequence of the current syntax. At the end of the day, a developer debugging any system that uses a framework may need to understand it’s internals to debug the issue. Reducing magic (or containing magic to a thin part of the framework) makes that step easier.

The kea internals will generate action objects from the `actions` call, and the `reducers` call provides a shorthand to responding to those actions. Listeners provides a way to spawn effects when an action occurs. These are compositions and abstractions over the Redux core. To figure out how they map to Redux, you need to either read the docs, or read the source.

I think you can map the example class I wrote to similar concepts - but instead of three+ different function calls to build a logic, you could do one, like `magical(classInstance)` - which would scan the instance and derive internal state machinery much the same way that kea’s `actions` and `reducers` iterate over their argument objects. Action properties translate to actions, state properties translate to reducers, and although listeners aren’t explicitly scannable, the effect callback inside an action can provide a similar API w/ breakpoint() within that scope.

Make the state properties only assignable inside an action function interior, and store their data inside Redux. Buffer writes to these properties until the end of the action internal function, and then flush them as a single Redux reducer change. Data mutation is bad - I agree! Deep-freeze the data those state properties point to in local development mode, and use a proxy that throws on invalid mutations like classInstance.users.push(…) to guide developers —- or use Immer as you suggest.

Dispatch calls to the action methods as Redux actions of the form { target: classInstance, action: methodName, payload: methodArgs }, and also build a reducer that will call the action function when the reducer receives that Redux action object.

This api is certainly more magical that kea’s api — but it’s still a composition on top of Redux. It has even more ”shorthand” for Redux. But would it be less maintainable? No matter the framework - Redux, Redux Toolkit, Kea, this monstrosity I just cooked up - your team needs to invest in culture and training about the right way to do things. There’s nothing AFAICT that kea-the-framework-code or redux-the-framework-code can do to prevent a developer from doing wild side-effects inside a reducer; likewise the sketch above needs culture and linters that guide developers to always wrap effects in action.effect, don’t use untracked private state, etc. Maybe the kea API structure makes it easier to build the cultural parts, but requires more boilerplate - that’s part of the trade off.

I guess the question I’m asking is, why is kea’s current position on the explicit-vs-magic spectrum the right global maximum for developer ergonomics & maintainability? kea advanced over raw Redux already, but why not go even further?

You're asking very good questions.

> I think you can map the example class I wrote to similar concepts - but instead of three+ different function calls to build a logic, you could do one, like `magical(classInstance)` - which would scan the instance and derive internal state machinery much the same way that kea’s `actions` and `reducers` iterate over their argument objects.

That's the magic of Kea: it gives you a framework you can use to get exactly the DSL that makes sense for your app.

This example code you posted:

    kea([
      actions({
        loadUsers: true,
        loadUsersSuccess: (users) => ({ users }),
        loadUsersFailure: (error) => ({ error }),
      }),
    
      reducers({
        users: [
          [],
          {
            loadUsersSuccess: (_, { users }) => users,
          },
        ],
        usersLoading: [
          false,
          {
            loadUsers: () => true,
            loadUsersSuccess: () => false,
            loadUsersFailure: () => false,
          },
        ],
        usersError: [
          null,
          {
            loadUsers: () => null,
            loadUsersFailure: (_, { error }) => error,
          },
        ],
      }),
    
      listeners(({ actions }) => ({
        loadUsers: async () => {
          try {
            const users = await api.get('users')
            actions.loadUsersSuccess(users)
          } catch (error) {
            actions.loadUsersFailure(error.message)
          }
        },
      })),
    ])
    
I'd personally write as this code:

  kea([
    loaders({
      users: { loadUsers: () => api.get('users') }
    })
  ])
and it would do exactly the same thing.

Well, almost, since the `usersError` message won't be saved. To get that you will need to add a reducer.

  kea([
    loaders({
      users: { loadUsers: () => api.get('users') }
    }),
    reducers({
      usersError: (_, { loadUsersFailure: (_, { error }) => error })
    }),
  ])
This is the magic Kea brings to the table, as you can use the core building blocks as frontend state management lego, creating an ever higher language with which to describe your application's logic.

Loading data is such a common pattern, that it makes sense to standardise on a framework level. Who knows what needs your app will have.

> But would it be less maintainable? No matter the framework - Redux, Redux Toolkit, Kea, this monstrosity I just cooked up - your team needs to invest in culture and training about the right way to do things.

You're hitting the nail on the head. I've worked with many developers using Kea over the last years, and while people get it quite quickly, you do need to actually read the docs to understand how it all works together. Otherwise you'll bring your old patterns to this new environment, and write bad (hard to maintain) code.

But this is the case no matter the framework, as you argue. If you don't put in the time in to learn the tooling, your output will be highly inefficient.

At work, I've spent quite a bit of time teaching colleagues how to write Kea the right way, and will now contribute most of this knowledge back in the form of screencasts and tutorials on keajs.org. My hope is that these will help future colleagues of mine level up faster, and the same would apply to all other teams using Kea.

> I guess the question I’m asking is, why is kea’s current position on the explicit-vs-magic spectrum the right global maximum for developer ergonomics & maintainability? kea advanced over raw Redux already, but why not go even further?

You need to draw lines somewhere, and I believe "base 3" (actions, reducers, selectors) is a good place for that. This is in contrast to "base 2" (get, set), which lures with its simplicity, but lead to abstractions that perform worse when you chunk concepts. That extra unit of reasoning ...

I have used it while trying to contribute to a project but I found it not that easy to use and quite confusing
3 re-writes from scratch in 6 years, I know front end is dynamic but to be useful ship a code and mantain it. Crap tool is better than unestable one.
lol.

First, it's 2 rewrites. Second, those were all 98% backwards compatible for the users (might need to upgrade plugins), and just refactored the internals.

But sure, let's all write perfect code on day 0, taking into account all the ways libraries you depend on (React) can develop in the future. It's not like "frontend" is a moving target or anything...

/s