

A kea
is two things:
- An extremely smart mountain parrot from New Zealand.
- An equally smart architecture for frontend webapps, built on top of React and Redux.
Try it out
Open the documentation (AKA demo app) and view its source on GitHub.
In the documentation you will find several examples with source. Check it out!
No, really, check out the docs!
The basics
You create and connect kea logic stores to your components like this:
import { kea } from 'kea'
@kea({
key: (props) => props.id,
path: (key) => ['scenes', 'homepage', 'slider', key],
actions: () => ({
updateSlide: index => ({ index })
}),
reducers: ({ actions, key, props }) => ({
currentSlide: [props.initialSlide || 0, PropTypes.number, {
[actions.updateSlide]: (state, payload) => payload.key === key ? payload.index % images.length : state
}]
}),
selectors: ({ selectors }) => ({
currentImage: [
() => [selectors.currentSlide],
(currentSlide) => images[currentSlide],
PropTypes.object
]
})
})
export default class Slider extends Component {
render () {
const { currentSlide, currentImage } = this.props
const { updateSlide } = this.actions
const title = `Image copyright by ${currentImage.author}`
return (
<div className='kea-slider'>
<img src={currentImage.src} alt={title} title={title} />
<div className='buttons'>
{range(images.length).map(i => (
<span key={i} className={i === currentSlide ? 'selected' : ''} onClick={() => updateSlide(i)} />
))}
</div>
</div>
)
}
}
For side effects, choose between thunks or sagas.
Using the kea-saga package. They will be started and terminated together with your component!
import 'kea-saga'
import { kea } from 'kea'
@kea({
key: (props) => props.id,
path: (key) => ['scenes', 'homepage', 'slider', key],
actions: () => ({
updateSlide: index => ({ index })
}),
reducers: ({ actions, key, props }) => ({
currentSlide: [props.initialSlide || 0, PropTypes.number, {
[actions.updateSlide]: (state, payload) => payload.key === key ? payload.index % images.length : state
}]
}),
selectors: ({ selectors }) => ({
currentImage: [
() => [selectors.currentSlide],
(currentSlide) => images[currentSlide],
PropTypes.object
]
}),
start: function * () {
const { updateSlide } = this.actions
console.log('Starting homepage slider saga')
while (true) {
const { timeout } = yield race({
change: take(action => action.type === updateSlide.toString() && action.payload.key === this.key),
timeout: delay(5000)
})
if (timeout) {
const currentSlide = yield this.get('currentSlide')
yield put(updateSlide(currentSlide + 1))
}
}
},
stop: function * () {
console.log('Stopping homepage slider saga')
},
takeEvery: ({ actions, workers }) => ({
[actions.updateSlide]: workers.updateSlide
}),
workers: {
updateSlide: function * (action) {
if (action.payload.key === this.key) {
console.log('slide update triggered', action.payload.key, this.key, this.props.id)
}
}
}
})
export default class Slider extends Component {
render () {
const { currentSlide, currentImage } = this.props
const { updateSlide } = this.actions
const title = `Image copyright by ${currentImage.author}`
return (
<div className='kea-slider'>
<img src={currentImage.src} alt={title} title={title} />
<div className='buttons'>
{range(images.length).map(i => (
<span key={i} className={i === currentSlide ? 'selected' : ''} onClick={() => updateSlide(i)} />
))}
</div>
</div>
)
}
}
When the logic grows too big, you may extract it from your components and give it a new home in logic.js
files.
export default kea({
path: () => ['scenes', 'homepage', 'index'],
actions: ({ constants }) => ({
updateName: name => ({ name })
})
})
import sceneLogic from './logic'
@sceneLogic
export default class HomepageScene extends Component {
render () {
const { name } = this.props
const { updateName } = this.actions
}
}
If you only wish to import some properties and actions from your logic stores, use the @connect
decorator or add connect: { props: [], actions: [] }
inside @kea({})
, like so:
import sceneLogic from './logic'
import sceneSaga from './saga'
@connect({
actions: [
sceneLogic, [
'updateName'
]
],
props: [
sceneLogic, [
'name',
'capitalizedName'
]
],
sagas: [
sceneSaga
]
})
export default class HomepageScene extends Component {
render () {
const { name } = this.props
const { updateName } = this.actions
}
}
Connecting to your app
Starting with 0.19
, all you need to do is to hook up redux
and redux-saga
as you normally would, and then just add keaReducer
and keaSaga
, like this:
import { keaSaga, keaReducer } from 'kea'
const reducers = combineReducers({
scenes: keaReducer('scenes'),
})
const sagaMiddleware = createSagaMiddleware()
const finalCreateStore = compose(
applyMiddleware(sagaMiddleware),
)(createStore)
const store = finalCreateStore(reducers)
sagaMiddleware.run(keaSaga)
Singleton and dynamic logic stores
If you specify the key
key in kea({})
, kea will create a dynamic logic store. Each component you connect it to, will have its own actions and reducers.
Omitting this key
key will create singletons. You can then export these singletons and connect to them as described above.
Redux all the way!
When you run kea({}), you get in return an object that exposes all the standard redux constructs.
export default kea({ ... })
import homepageLogic from '~/scenes/homepage/logic'
homepageLogic.path === ['scenes', 'homepage', 'index']
homepageLogic.selector === (state) => state.scenes.homepage.index
homepageLogic.actions === { updateName: (name) => { ... }, increaseAge: (amount) => { ... }, ... }
homepageLogic.reducer === function (state, action) { ... }
homepageLogic.selectors === { name: (state) => state.scenes.homepage.index.name, capitalizedName: ... }
homepageLogic.saga === function * () { ... }
Sagas
You may also create sagas and connect other actions using kea({})
:
import { kea } from 'kea'
import sceneLogic from '~/scenes/homepage/logic'
import sliderLogic from '~/scenes/homepage/slider/logic'
export default kea({
connect: {
actions: [
sceneLogic, [
'updateName',
'increaseAge',
'decreaseAge'
],
sliderLogic, [
'updateSlide'
]
]
},
takeEvery: ({ actions, workers }) => ({
[actions.updateName]: workers.nameLogger,
[actions.increaseAge]: workers.ageLogger,
[actions.decreaseAge]: workers.ageLogger
}),
start: function * () {
const { updateSlide } = this.actions
while (true) {
const { timeout } = yield race({
change: take(updateSlide),
timeout: delay(5000)
})
if (timeout) {
const currentSlide = yield sliderLogic.get('currentSlide')
yield put(updateSlide(currentSlide + 1))
}
}
},
stop: function * () {
console.log('Closing saga')
},
workers: {
nameLogger: function * (action) {
const { name } = action.payload
console.log(`The name changed to: ${name}!`)
},
ageLogger: function * (action) {
const age = yield sceneLogic.get('age')
console.log(`The age changed to: ${age}!`)
}
}
})
Read the documentation for redux-saga
/
Scaffolding (work in progress)
To get started with a new kea project, just type these commands:
npm install kea-cli -g
kea new my-project
cd my-project
npm install # or yarn
npm start # or yarn start
and open http://localhost:2000/.
Later inside my-project
run these to hack away:
kea g scene-name # new scene
kea g scene-name/component-name # component under the scene
kea g scene-name/component-name/really-nested # deeply nested logic
More documentation coming soon! Please help if you can!