Hacker News new | comments | show | ask | jobs | submit login
Show HN: Kea – The power of Redux with the simplicity of MobX (js.org)
60 points by mariusandra 9 months ago | hide | past | web | favorite | 39 comments



Looks pretty neat. My initial reactions:

- 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)


>- 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.

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;


I'm a big fan of decorators, but it seems like much of the React/Redux world is overly class-decorator happy. It's really common to define properties in a decorator which would be much easier to read, IMO, as static or instance properties. I'd rather read something more like this:

    @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>
        )
      }
    }


Hey, thanks for the feedback! This is definitely something to think about, as nothing in the syntax of kea is set in stone.

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.


> 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

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...


It's worth noting that both the React and Redux teams generally discourage the use of decorators. Quoting the Create-React-App README:

> 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.


Hey, 1) As munchor pointed out, you don't need to use the decorators. See the "what is this @kea()" button in the first guide for more details: https://kea.js.org/guide/counter

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.


In my experience action creators and actions in the reducer don't necessarily map 1 to 1, and often don't. I think of action creators as the API to the reducer. This makes a lot more sense when using redux-thunk and similar, of course.


Thanks for sharing those functions. Definitely cuts down on a lot of the boilerplate I see with Redux.


Also worth noting that the Redux docs _specifically_ have a section called "Reducing Boilerplate" [0] that shows how to write functions like this, and my Redux addons catalog [1] lists dozens of such libraries already.

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

[1] https://github.com/markerikson/redux-ecosystem-links


I know it's just an example... But the Slider code is ridiculously overwrought.

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.


I can't really speak for the Kea library directly, but the patterns used in React/Redux land are most valuable in large, evolving projects spanning many developers. Such benefits are therefore subtle and difficult to illustrate with concise examples.

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.


Hey, indeed it's an example, as you point out. This was the best example I could come up with that demonstrated most of the functionality available, yet remained rather small in size.

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.


Just to clarify, I'm not picking on you; I realize it's hard to come up with meaningful examples.

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.


It's an example. Do you also comment on new programming languages and say "jeez, all that code to say "hello world"!

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.


See my other reply below: 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.

The scare quotes are because it's hardly a library when it provides less than 5 lines of code, as you noted.


> 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.


As you indicate, it's just an example. These libraries aren't for building image sliders, they're for building complex applications that you can't easily provide examples for.


Shameless plug for another take on the same promise: https://github.com/jnorth/megalith

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.


If my favorite part of redux is structuring flows with redux-saga, does mobx-state-tree have a solution for me?

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!)


This example doesn't cover all the cases you're showing above, but in megalith or MST you would typically have a clearer separation between the functions that change state and the async communication parts.

https://github.com/jnorth/megalith/blob/master/docs/examples...

https://github.com/mobxjs/mobx-state-tree#creating-async-pro...


Yeah, I also like to use redux with a clear separation between actions that change state and those that don't. (For example, reducers only take actions prefixed by DATA_ or UI_, all other actions...the bulk of them really...don't have anything to do with changing state and are handled by other processes. But because of this I don't bother with action creators, so what do I know!)

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.


Right, maybe I called out the wrong part of your example. redux-saga gives a really elegant way to describe the flow, but I feel at the expense of wrapping the async code with special helpers (call, put, fork, etc). You have to learn a few extra concepts for it to be comfortable. Async code in MST and megalith interact a bit more directly with the stores.

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 :)


MST doesn't have the same restrictions as redux in terms of async so you don't need to do it in the same way.

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
            }
        }
    }


Looks nice. Seems to do a good job of making the code easy to read.

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.


Yeah, if I was starting today I might end up with MST as well. The only real benefit of megalith right now would be its size (5k vs 45k). You do get more features for that size though, like integration with the Redux toolset.


I guess you miss out on all the nice mobx stuff too (computed properties etc).

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.


Thanks!

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 :)


I’ve made a lightweight React library influenced by functional setState, and supports async/await/Promises, extracting values from DOM / forms easily, pure function actions, animation with requestAnimationFrame, and reloading when props change.

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!


Looks nice, but I think I can get most of it using recompose, which I've been using extensively lately.


Yes, you would be able to do a lot with the flexible recompose. I’ve tried to make something more friendly and lightweight that makes common use cases easier. I can find recompose a little overwhelming in the multiple layers.


Funny, I always thought of Redux as offering greater simplicity and MobX being more powerful.


With great simplicity comes great power ;)

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...


Is this type-safe? Can it infer the payload type somehow? With MobX you can write type-safe code, and it's pretty important in bigger code bases. Redux can be made type-safe but is a bit clunky to use that way.


Hi, propTypes are supported as an (optional) first class citizen. Thus whatever is the output of a reducer or a selector, provided it is passed to a react component, is type checked.

That said, I haven't used flow or TS myself and can't confirm nor deny compatibility


The concept expressed in the title matches a desire. Looking forward to checking out and hearing other's experiences.


Why not just use state, which seems like the correct solution to the examples?


Because the examples are just that, examples. Kea is designed for large complicated apps, and is in production use for at least 4 apps that I'm aware of (from marketplaces to fleet tracking)


I have to dig into this but at first glance I like the approach




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: