Example #5 - Connected logic

Attaching actions, reducers and selectors to your components will get you pretty far.

However, there comes a time in every application's life when that's no longer enough. Perhaps you need to share code between components? Perhaps you want to separate concerns to make it more readable? Perhaps the logic has grown over 200 lines and really should be yanked out of index.js?

Whatever the reason, you're in luck! Kea was originally built just for this. The inline version that we've been using until now (@kea({})(Component)) came much later.

So how do you separate your logic from your components?

Let's look at a real world example: this guide itself.

Separating concerns

You might have noticed buttons that look like this:

How do they work?

When I started writing the guide, I just put this code on top of every component:

// index.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { kea } from 'kea'

@kea({
  actions: () => ({
    toggleFeature: (feature) => ({ feature })
  }),
  reducers: ({ actions }) => ({
    features: [{}, PropTypes.object, {
      [actions.toggleFeature]: (state, payload) => {
        const { feature } = payload
        return {
          ...state,
          [feature]: !state[feature]
        }
      }
    }]
  })
})
export default class CounterExampleScene extends Component {
  // ...

  render () {
    const { features } = this.props
    const { toggleFeature } = this.actions

    return (
      <div>
        <button onClick={() => toggleFeature('reducerDetails')}>{'Tell me more!'}</button>

        {features.reducerDetails ? (
          <div className='extra-help'>
            extra help comes here
          </div>
        ) : null}
      </div>
    )
  }
}

The code is rather straightforward, perhaps just a little bit unfamiliar.

We have an object, features, which contains booleans that can be toggled on and off. We return a new object every time the action toggleFeature(feature) is called, flipping the boolean for the requested feature.

While it worked fine, it became repetetive to copy this same code on top of every page in the guide. Code duplication is usually a and should be handled approriately.

So how do we stop this madness?

First we must extract the logic to a separate file. Let's call it features-logic.js:

// features-logic.js
import PropTypes from 'prop-types'
import { kea } from 'kea'

export default kea({
  actions: () => ({
    toggleFeature: (feature) => ({ feature })
  }),
  reducers: ({ actions }) => ({
    features: [{}, PropTypes.object, {
      [actions.toggleFeature]: (state, payload) => {
        const { feature } = payload
        return {
          ...state,
          [feature]: !state[feature]
        }
      }
    }]
  })
})

You skip the @ in front of @kea, but everything else remains the same.

Then we have a few ways to import it.

The simplest is to do a drop-in replacement:

// index.js
import React, { Component } from 'react'

import featuresLogic from '../features-logic'

@featuresLogic
export default class CounterExampleScene extends Component {
  // no change here
}

That's simple and works fine, but will only get you so far. There are a few things wrong with this approach:

First, it's not clear what you're importing. As code is read far more times than it's written, it's very very useful to be as explicit as possible. Your future self will thank you immensely! In this case you have no idea what extra props and actions your component will receive without having to open features-logic.js and checking it out yourself.

You also don't know what baggage you're getting. There might be hundreds of new actions and props, which your relatively simple component will receive. That will slow down the page, as a change in each of them will trigger re-rendering.

What if one of the props changes? For example the new intern replaces the action toggleFeature with two new actions: setFeature and clearFeature. Without (manually or automatically) clicking the buttons, you won't know that the action is no longer there. Bummer.

What's more, you can't chain calls like this and you can't add other actions that only this component will use.

So what's the solution? Answer: the @connect({ ... }) helper!

import React, { Component } from 'react'
import { connect } from 'kea'

import featuresLogic from '../features-logic'
import someOtherLogic from '../some-other-logic'

@connect({
  actions: [
    featuresLogic, [
      'toggleFeature'
    ]
  ],
  props: [
    featuresLogic, [
      'features'
    ],
    someOtherLogic, [
      'isMenuOpen',
      'isPageLoading',
      'highlightTheme'
    ]
  ]
})
export default class CounterExampleScene extends Component {
  // as we were...
  render () {
    const { features, isMenuOpen, isPageLoading, highlightTheme } = this.props
    const { toggleFeature } = this.actions
    // ...
  }
}

While the syntax may look alien at first, it's very comfortable to use and optimised for readability.

With connect you may import as many props and actions from as many logic stores as you like. In case you make a typo (highlghtTheme instead of highlightTheme), you will get an error in the JS console and can fix it immediately. Just connect your logic and use it!

What if you also want to add actions and reducers only this component will use?

No problem! Just replace @connect with @kea({ connect: {}, /* other stuff */ }) like so:

import React, { Component } from 'react'
import { connect } from 'kea'

import featuresLogic from '../features-logic'

@kea({
  connect: {
    actions: [
      featuresLogic, [
        'toggleFeature'
      ]
    ],
    props: [
      featuresLogic, [
        'features'
      ]
    ]
  },
  actions: {
    doSomething: (id) => ({ id })
  },
  // ...
})
export default class CounterExampleScene extends Component {
  // no change here
}

If this is not redux-heaven, I don't know what is!

Next page: Dynamic Connected Logic