Sagas

Kea has first class support for sagas via the kea-saga plugin.

Read more about Sagas on the redux-saga homepage.

Also read the sections of the guide marked "with kea-saga" to learn more.

Installation

First install the kea-saga and redux-saga packages:

yarn add kea-saga redux-saga
npm install --save kea-saga redux-saga

Then you have a few ways to install the plugin:

// the cleanest way
import sagaPlugin from 'kea-saga'
import { getStore } from 'kea'

const store = getStore({
  plugins: [ sagaPlugin ]
})

// another way
import sagaPlugin from 'kea-saga'
import { activatePlugin } from 'kea'

activatePlugin(sagaPlugin)

// the shortest way
import 'kea-saga/install'

Use whichever way is most convenient for your setup.

Note that the kea-saga plugin needs to be installed globally as it needs to add its middleware to the store. You can't use it as a local plugin inside kea({}) calls.

If you have configured your store through getStore(), you're all set!

Usage

First, read the docs on the redux-saga homepage to learn how sagas work.

Adding kea-saga will give your logic stores access to the keys: start, stop, takeEvery, takeLatest, workers, sagas.

import { kea } from 'kea'

export default kea({
  // ... see the api docs for more

  start: function * () {
    // saga started or component mounted
    console.log(this)
  },

  stop: function * () {
    // saga cancelled or component unmounted
  },

  takeEvery: ({ actions, workers }) => ({
    [actions.simpleAction]: function * () {
      // inline worker
    },
    [actions.actionWithDynamicPayload]: workers.dynamicWorker
  }),

  takeLatest: ({ actions, workers }) => ({
    [actions.actionWithStaticPayload]: function * () {
      // inline worker
    },
    [actions.actionWithManyParameters]: workers.dynamicWorker
  }),

  workers: {
    * dynamicWorker (action) {
      const { id, message } = action.payload // if from takeEvery/takeLatest
      // reference with workers.dynamicWorker
    },
    longerWayToDefine: function * () {
      // another way to define a worker
    }
  },

  sagas: [saga1, saga2]
})

start: function * () {}

Saga that is started whenever the component is connected or the saga exported from this component starts

Note: sagas are started before your wrapped component's componentDidMount. Actions dispatched before this lifecycle method will not be seen inside start.

// Input
start: function * () {
  // saga started or component mounted
  console.log(this)
}

// Output
myRandomSceneLogic.saga == function * () {
  // saga started or component mounted
  console.log(this)
  // => { actions, workers, path, key, get: function * (), fetch: function * () }
}

stop: function * () {}

Saga that is started whenever the component is disconnected or the saga exported from this component is cancelled

This function is called right before your wrapped component's componentWillUnmount lifecycle method.

// Input
stop: function * () {
  // saga cancelled or component unmounted
}

// Output
myRandomSceneLogic.saga == function * () {
  try {
    // start()
  } finally {
    if (cancelled()) {
      // saga cancelled or component unmounted
    }
  }
}

takeEvery: ({ actions }) => ({})

Run the following workers every time the action is dispatched

Note: sagas are started before your wrapped component's componentDidMount. Actions dispatched before this lifecycle method will not be seen by takeEvery.

// Input
takeEvery: ({ actions, workers }) => ({
  [actions.simpleAction]: function * () {
    // inline worker
  },
  [actions.actionWithDynamicPayload]: workers.dynamicWorker
})

// Output
myRandomSceneLogic.saga == function * () {
  // pseudocode
  yield fork(function * () {
    yield [
      takeEvery(actions.simpleAction.toString(), function * () {
        // inline worker
      }.bind(this)),
      takeEvery(actions.actionWithDynamicPayload.toString(), workers.dynamicWorker.bind(this))
    ]
  })
}

takeLatest: ({ actions }) => ({})

Run the following workers every time the action is dispatched, cancel the previous worker if still running

Note: sagas are started before your wrapped component's componentDidMount. Actions dispatched before this lifecycle method will not be seen by takeLatest.

// Input
takeLatest: ({ actions, workers }) => ({
  [actions.simpleAction]: function * () {
    // inline worker
  },
  [actions.actionWithDynamicPayload]: workers.dynamicWorker
})

// Output
myRandomSceneLogic.saga == function * () {
  // pseudocode
  yield fork(function * () {
    yield [
      takeLatest(actions.simpleAction.toString(), function * () {
        // inline worker
      }.bind(this)),
      takeLatest(actions.actionWithDynamicPayload.toString(), workers.dynamicWorker.bind(this))
    ]
  })
}

workers: {}

An object of workers which you may reference in other sagas.

// Input
workers: {
  * dynamicWorker (action) {
    const { id, message } = action.payload // if from takeEvery/takeLatest
    // reference with workers.dynamicWorker
  },
  longerWayToDefine: function * () {
    // another worker
  }
}

// Output
myRandomSceneLogic.workers == {
  dynamicWorker: function (action) *
    const { id, message } = action.payload // if from takeEvery/takeLatest
    // reference with workers.dynamicWorker
  }.bind(myRandomSceneLogic),

  longerWayToDefine: function () * {
    // another worker
  }.bind(myRandomSceneLogic)
}

sagas: []

Array of sagas that get exported with this component's saga

// Input
sagas: [saga1, saga2]

// Output
myRandomSceneLogic.saga == function * () {
  yield fork(saga1)
  yield fork(saga2)

  // start() ...
}

FAQ

My sagas won't start with my component!

kea-saga injects itself into your component's componentDidMount function and starts the sagas before returning control to the original componentDidMount.

Because of the way ES classes work, if you define your componentDidMount as an arrow function (componentDidMount = () => {}), it will only be declared after your class is instantiated and it will overwrite modifications by kea-saga.

Thus, at least for now, keep your componentDidMount as a regular function. See more details in this issue.