Show HN: Kea – The power of Redux with the simplicity of MobX
kea.js.org
kea.js.org
- I don't like the `@kea(...` because it's another bit of JS syntax I hadn't heard of before. Though I do like decorators in Python, I just feel like I have learned enough of JS syntax to be productive and they seem to keep adding unnecessary stuff.
- There is still duplication between the action names and the reducers. When using Redux I now always use these two functions:
function makeReducer(reducerObject) {
return (state, action) => {
if (Object.keys(reducerObject).includes(action.type)) {
return reducerObject[action.type](state, action.value)
}
return state
}
}
function makeActions(reducerObject) {
const actions = {}
Object.keys(reducerObject).forEach(name => {
actions[name] = value => {
return {type: name, value}
}
})
return actions
}
So I can just declare a single object of reducers and can infer the action names and creators from that e.g. const reducerObject = {
increment(state, value) {
return state + 1
},
decrement(state, value) {
return state - 1
}
}
const reducer = makeReducer(reducerObject)
const actions = makeActions(reducerObject)You can just not use the @ syntax for decorators and instead just do it manually like so:
class Counter extends Component {
...
}
Counter = kea(...)(Counter);
export default Counter; @kea // kicks off the meta-programming to read the statics below
export default class Counter extends Component {
static actions = {
increment: (amount) => ({ amount }),
decrement: (amount) => ({ amount }),
};
static reducers = {
counter: [0, PropTypes.number, {
[this.actions.increment]: (state, payload) => state + payload.amount,
[this.actions.decrement]: (state, payload) => state - payload.amount
}],
};
static selectors = ({ selectors }) => ({
doubleCounter: [
() => [selectors.counter],
(counter) => counter * 2,
PropTypes.number,
],
});
render () {
const { counter, doubleCounter } = this.props
const { increment, decrement } = this.constructor.actions
return (
<div className='kea-counter'>
Count: {counter}<br />
Doublecount: {doubleCounter}<br />
<button onClick={() => increment(1)}>Increment</button>
<button onClick={() => decrement(1)}>Decrement</button>
</div>
)
}
}That said, the magic with having a big options hash in the `@kea({})` call itself is that then you can easily disconnect the logic from your components as your application grows. Here's more information about this: https://kea.js.org/guide/connected
Also, I'm not sure you can do `this.actions` inside a static variable, but I understand the point that you were trying to make.
Interesting. I still think these are better modeled (and read) as part of the class declaration. You can do the same thing with mixins:
Transforming that example:
import FeaturesLogic from '../features-logic.js';
@kea
class CounterExampleScene extends FeaturesLogic(Component) {
// no change here
}
Where FeaturesLogic is a JS class mixin: http://justinfagnani.com/2015/12/21/real-mixins-with-javascr... export const FeaturesLogic = (base) => class extends base {
static actions = ...;
}
> Also, I'm not sure you can do `this.actions` inside a static variable, but I understand the point that you were trying to make.Not in the current Babel transform, but I thought it may be in the latest unified class fields proposal. I'm digging for it...
> Create React App doesn’t support decorator syntax at the moment because:
> - It is an experimental proposal and is subject to change.
> - The current specification version is not officially supported by Babel.
> - If the specification changes, we won’t be able to write a codemod because we don’t use them internally at Facebook.
> Create React App will add decorator support when the specification advances to a stable stage.
In addition, use of the React-Redux `connect` method as a decorator can lead to unexpected behavior, such as `defaultProps` applying to the generated wrapper component instead of the underlying "plain" component. I'm not sure how the `kea` decorator handles that sort of thing.
2) Indeed there is duplication between action names and the reducers. This however makes it so that you can have many reducers listening to the same actions. E.g. setting `isLoading` to `false` when `actions.resultsFetched` occurs.
As I've said many times: it's entirely up to you how much abstraction you want to apply on top of Redux.
[0] http://redux.js.org/docs/recipes/ReducingBoilerplate.html
Building this type of image carousel is trivial with just plain HTML+CSS+JS. This example uses "actions", "reducers", "selectors", weird functions from something called "redux-saga/effects", a "promise-based timeout" library AND generator functions to produce the magic ... of switching between images.
If someone I work with came up with this code, I would assume that the person is extremely bored with her job and wants to spruce up her résumé with buzzwords.
It's easy to dismiss these things as "buzzword" soup when the examples are trivial for illustrative purposes, but having real experience employing these patterns at scale is extremely valuable in the real world.
The point was to show what this library is capable of, so that people with the imagination of your imaginary co-worker could extrapolate its usefulness to real world applications.
I have built 3 large applications using kea, and at that scale all these features come in handy.
The problem with Slider is that it doesn't readily extrapolate into anything bigger. To put it another way, it's not the embryo of any real world web app.
I feel this indicates a larger problem with present-day web dev: due to the complexity of client and server stacks, it's really hard to come up with a self-contained example app that makes any sense as a starting point.
Looking back some 20 years, one could look at the example apps from the Win32, Classic MacOS and OpenStep SDKs, and come out with a solid feel for what it's like to build an app on each platform.
Today, I could compare the provided examples from twelve different JavaScript frameworks, and I wouldn't feel any closer to understanding their strengths.
Why the scare quotes around "promise-based timeout" library? I can guess just by looking at the code that it's a Promise wrapper around setTimeout. It makes the example easier to read, since the API is based around promises and not callbacks. It's probably less than 5 lines of code.
The scare quotes are because it's hardly a library when it provides less than 5 lines of code, as you noted.
It makes the example easier to read for me, because I can figure out what it does by the context:
1. It's called "delay"
2. It's a function that takes 1 parameter, a number
3. There is a browser API called setTimeout, which takes a function and a number
4. Converting functions that take a callback to a function that returns a promise is a common pattern.
I know what delay does without even needing to see it. Maybe if you don't know the 4 things I've listed above, this makes the example harder to read, but then you probably do not need this library.
> I feel Slider is not a useful example because it doesn't extrapolate into any kind of real world web app -- which is a problem when this library is about helping you build real world web apps.
It's an example on the homepage, not documentation. How many lines of code do you want to see? I feel like if you assume that the author wrote this to scratch their own itch, then you can use your imagination and assume that they work on apps with complicated state that needs to be persisted over a network, that gets fed to functional components.
Edit: I read your other comment, and I agree it's really hard to come up with good, non-trivial examples.
I think most people who are attracted to Redux's core principals but want a more opinionated structure will end up gravitating towards mobx-state-tree though.
I'm assuming you started this before mobx state tree? It looks very similar. After much experimentation, MST is where I've decided to spend my time. Like your library, the code ends up readable.
Having said that, I have to commend you. Your state library is the cleanest I've seen in terms of the user land code. There's nothing in the examples that feels like framework noise.
Yeah, right now, state that shouldn't be saved (like computed properties) can be added as regular object properties (i.e. not a part of the initialState), and updated by listening for action events.
I've been thinking of ways to make that a little easier, possibly by having "reactions" that execute in response to actions. But I haven't had the need in my own projects yet, and like to err on the side of a smaller API footprint :)
For example, this is nice:
// On login, spin off the auth process. If an error or a logout happens, clean up.
// Be sure to cancel any pending auth request!
// Then get ready for a new login.
function* loginFlow() {
while (true) {
const {user, password} = yield take('LOGIN_REQUEST')
const task = yield fork(authorize, user, password)
const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
if (action.type === 'LOGOUT')
yield cancel(task)
yield call(Api.clearItem, 'token')
}
}
(Genuine question...idk!)Edit: In MST it would look more like plain js:
class AuthStore() {
....
{
login() {
do_async_thing()
.then(this.postLoginStuff)
.catch(this.handleFailedLogin)
},
logout() {
// do logout stuff
}
}
}https://github.com/jnorth/megalith/blob/master/docs/examples...
https://github.com/mobxjs/mobx-state-tree#creating-async-pro...
A conceptual appeal of mobx to me is that this distinction seems baked in. The original mobx didn't click with me like redux did ¯\_(ツ)_/¯, but if MST gives transactional updates and support for redux dev tools (right?) could be worth trying.
One thing I tried to do with megalith is mapping ideas from Redux to built-in language features as much as possible. An action object can trivially be represented as a function call for example—action name and payload is mapped to function name and arguments.
I think Mobx gets into trouble there by building off of observables—you have to litter your async code with different types of action markers to keep state consistent. MST tries to corral all those variations down into a smaller set, where the Redux ecosystem is layering ideas on top of an small initial core. Megalith and MST are starting from two completely opposite sides, but end up looking very similar somewhere in the middle :)
I’ve put a lot of effort into providing some good examples (more coming!), and also on keeping it lightweight: core is 1.18KB gzipped.
https://github.com/RoyalIcing/react-organism
Feedback welcome, here or as issues!
MobX is simple to get going, but I fear there are dragons beneath. Here's a small writeup I did about it on Reddit: https://www.reddit.com/r/reactjs/comments/6pni2i/kea_high_le...
That said, I haven't used flow or TS myself and can't confirm nor deny compatibility