Kea v0.17.1

Maintainable Redux

Redux brought the best paradigms of functional programming into frontend web development. By reducing the amount of operations you perform on your data, you greatly increase the maintability of your application, leading to clean and bug-free code.

However, unlimited liberty comes at a cost. Setting up your project can take days. Finding all the right pieces can take weeks. Thunk? Saga? Act? Rx? Ducks? Promises? Routes? Bacon? Immutable? Reselect? Reactuate? Reswitch? ...

Kea provides an unified approach to organising your application, making Redux in practice as easy to use as MobX, but without all the edge cases.

We do this by separating your data into Logic Stores, connecting them to your Components and orchestrating side-effects via Saga Classes.

This separation of concerns brings clarity and maintainability.

Kea is already used in production for large websites, from two-sided marketplaces to fleet tracking. It's battle tested and it works.

1. Logic Stores

Logic Stores are to data what JSX is to templates.

Instead of splitting your action creators, constants, reducers and selectors over 8 files in 4 directories, combine them into clear and consise Logic Stores.


  import Logic, { initLogic } from 'kea/logic'
  import { PropTypes } from 'react'

  @initLogic
  export default class HomepageLogic extends Logic {
    path = () => ['scenes', 'homepage', 'index']

    actions = ({ constants }) => ({
      updateName: (name) => ({ name }),
      increaseAge: (amount = 1) => ({ amount }),
      decreaseAge: (amount = 1) => ({ amount })
    })

    reducers = ({ actions, constants }) => ({
      name: ['Chirpy', PropTypes.string, {
        [actions.updateName]: (state, payload) => payload.name
      }],

      age: [3, PropTypes.number, {
        [actions.increaseAge]: (state, payload) => state + payload.amount,
        [actions.decreaseAge]: (state, payload) => Math.max(state - payload.amount, 1)
      }]
    })

    selectors = ({ selectors, constants }) => ({
      capitalizedName: [
        () => [selectors.name],
        (name) => name.trim().split(' ').map(k => (
                    `${k.charAt(0).toUpperCase()}${k.slice(1).toLowerCase()}`
                  )).join(' '),
        PropTypes.string
      ],

      description: [
        () => [selectors.capitalizedName, selectors.age],
        (capitalizedName, age) => `Hello, I'm ${capitalizedName}, a ${age} years old bird!`,
        PropTypes.string
      ]
    })
  }


This specific example exports 3 action creators (updateName, increaseAge, decreaseAge) and four selectors (name, age, capitalizedName, description).

Just import homepageLogic from './logic' and use all of them directly!

2. Component connection

Now that your data is defined, just @connect the required properties and actions to your components:


  import { connect } from 'kea/logic'

  import Slider from '~/scenes/homepage/slider'

  import sceneLogic from '~/scenes/homepage/logic'
  import sliderLogic from '~/scenes/homepage/slider/logic'

  @connect({
    actions: [
      sceneLogic, [
        'updateName'
      ]
    ],
    props: [
      sceneLogic, [
        'name',
        'capitalizedName'
      ],
      sliderLogic, [
        'currentSlide',
        'currentImage'
      ]
    ]
  })
  export default class HomepageScene extends Component {
    updateName = () => {
      const { name } = this.props
      const { updateName } = this.props.actions

      const newName = window.prompt('Please enter the name', name)

      if (newName) {
        updateName(newName) // no need for dispatch
      }
    }

    render () {
      const { capitalizedName, currentSlide, currentImage } = this.props

      return (
        <div className='homepage-scene'>
          <Slider />
          <h1>
            Hello, I am <em onClick={this.updateName}>{capitalizedName}</em> the Kea
          </h1>
          <p>
            You are viewing image #{currentSlide + 1},
            taken by <a href={currentImage.url}>{currentImage.author}</a>
          </p>
        </div>
      )
    }
  }

3. Saga Classes

Sagas are wonderful for handling side-effects, but they can feel a little bit lost in your application.

Kea Saga Classes provide a unified interface to orchestrate them.


  import Saga from 'kea/saga'

  import sceneLogic from '~/scenes/homepage/logic'
  import sliderLogic from '~/scenes/homepage/slider/logic'

  export default class HomepageSaga extends Saga {

    // pull in actions from logic stores
    actions = () => ([
      sceneLogic, [
        'updateName',
        'increaseAge',
        'decreaseAge'
      ],
      sliderLogic, [
        'updateSlide'
      ]
    ])

    // run a worker when an action is called
    takeEvery = ({ actions }) => ({
      [actions.updateName]: this.nameLogger,
      [actions.increaseAge]: this.ageLogger,
      [actions.decreaseAge]: this.ageLogger
    })

    // main loop of saga
    // - update the slide every 5 sec
    run = function * () {
      // to ease readability we always list the actions we use on top
      const { updateSlide } = this.actions

      while (true) {
        // wait for someone to call the updateSlide action or for 5 seconds to pass
        const { timeout } = yield race({
          change: take(updateSlide),
          timeout: delay(5000)
        })

        // if timed out, advance the slide
        if (timeout) {
          // you can the contents of a logic store instance via "yield logic.get('property')"
          const currentSlide = yield sliderLogic.get('currentSlide')
          // dispatch the updateSlide action
          yield put(updateSlide(currentSlide + 1))
        }

        // re-run loop - wait again for 5 sec
      }
    }

    // clean up if needed
    cancelled = function * () {
      console.log('Closing saga')
    }

    // on every updateName
    nameLogger = function * (action) {
      const { name } = action.payload
      console.log(`The name changed to: ${name}!`)
    }

    // on every increaseAge, decreaseAge
    ageLogger = function * (action) {
      const age = yield sceneLogic.get('age')
      console.log(`The age changed to: ${age}!`)
    }
  }


    

... and a lot more

Scenes, Routes, Scaffolding, etc.

Check out the project page on Github for more!