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 within the core abstraction gives a lot. Think 3*3*3=27 vs 2*2*2*2=16.
I get this is going very philosophical/abstract, but we're discussing "magic", so I think we're good :D.
Having seen a lot of different ways to code frontend over the years, I believe a redux-like pattern is the best fit for frontend apps, all* things considered. Compared to "base 2" (get/set) through regular JS primitives, the big difference is that redux forces you to label each write operation with an action type. Every write. This enables a whole class of side effects and observability. These inherent benefits of actions can then be described as magic that comes for free with the abstraction, not as magic that comes from building clever code that hides a complex implementations. As a developer, the former is so much easier to truly and intuitively master, while the latter sounds like a bag of edge cases that you'll keep poking with a stick (see: apollo, based on what I've read on HN lately).
Add to that a global immutable state tree that you can just pass around in a variable (the store), a system of labelled data retrievers from this state tree (selectors), and a way to connect all of these pipes directly to React, an auxiliary system of props and keys to fork data and its operations, a standard library of utils for common operations (routing, forms), commitment to compact syntax, a great way to compose code, and an ever expanding set of learning materials.
The result is what is Kea now. I'm not forcing anyone to use it, but I do think it strikes very close to the global maximum of developer ergonomics and maintainability.
I'm always happy to improve it, and Kea 3.0 was an exercise to push that envelope even further.