The principal operation of a website is to stream data between endpoints. Bytes start at the user's keyboard, pass through layers of application logic, land in the database and return back as impeccable HTML and CSS constructions.
kea-logic
makes these transformations a joy to perform on the frontend. It uses components you know and love (react
, redux
, redux-saga
, reselect
, react-router
) to create a well-oiled machine that brings your data to life.
There are many other projects under the kea
name that you may want to check out.
kea-logic
kea-logic
gives you logic stores with actions. These act as glue between React components and sagas.
The logic stores are designed to be well-readable, self-documenting and easily refactorable. You'll know how it works even if you have never seen the code before.
In addition to this, kea-logic
provides helpers to simplify routing, code splitting and other parts of a good webapp.
Try it out!
Open the demo app, browse its code and read below for an explanation of the parts.
To run the example on your machine, just type these commands:
npm install kea -g
kea new my-project
cd my-project
npm 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
Logic stores
Logic stores consist of 1) actions, 2) structure (reducer + selector + proptype), and 3) optionally sagas.
Logic stores are connected through ES6 imports.
Let's first look at a React component that uses these stores:
// scenes/homepage/index.js - This the root component for the homepage scene. skipping some imports // A helper component. // Note, you should always import with the full path, so you can easily move things around// and refactor just by searching for the path. // logic stores: 1) for this "homepage" scene root component and 2) the slider helper component // select which fields of data and which actions we want from the above imported logic storesconst mapping = actions: sceneLogic 'updateName' props: sceneLogic 'name' 'capitalizedName' sliderLogic 'currentSlide' 'currentImage' // react will know the PropTypes automatically static propTypes = // binding to 'this', hence the fat arrow syntax // this way we can just pass onClick={this.updateName} in render() { // each function defines on top which props and actions it needs const name = thisprops const updateName = thispropsactions const newName = windowprompt'Please enter the name' name if newName // call the action to update the data } // render the component { // the data we need from the imported stores const capitalizedName currentSlide currentImage = thisprops return <div className='homepage-scene'> <Slider /> <h1> Hello I am <em onClick=thisupdateName>capitalizedName</em> the Kea </h1> <p> You are viewing image #currentSlide + 1 taken by <a href=currentImageurl>currentImageauthor</a> </p> </div> } // finally, connect the mapping to the scenemappingHomepageScene
Logic stores consist of many parts:
They all have a path in the redux tree.
// scenes/homepage/logic.js // PATH 'scenes' 'homepage' 'index'
All the data declared below will live in your redux store under scenes.homepage.index
.
The action generators are inspired by redux-act
, but don't need a description. Give them a keyword and the payload object generator.
// ACTIONS name amount amount // Calling `updateName('new name')` will create an object like: // { type: 'updateName@homepage.index', payload: { name: 'new name' } }
Logic stores have a structure, created with redux
and reselect
plus type declarations and optional persistence. Everything here is a pure function working with immutable data.
// STRUCTURE name: age:
Finally, we add custom reselect
selectors to transform and cache data:
// SELECTORS { // define the name of the selector, its PropType and tell it what data you want }}
Logic stores are exported as singletons from these logic.js
files:
Check out the TodoMVC logic.js for a more complete example.
Once defined, a logic store can be imported in any component, saga or other logic store as needed.
// use them just like thissceneLogicpath === 'scenes' 'homepage' 'index'sceneLogicactions === , increaseAge: function(amount), ... }sceneLogicreducer === { ... }sceneLogicselector === statesceneshomepageindexsceneLogicselectors === statesceneshomepageindexname ... // or plug them into other kea-logic components for maximum interoperability
Side effects (API calls, etc)
Since logic stores are strictly composed of pure functions that operate on immutable data, we need an alternative place to handle all of the messy business logic of the app.
Enter sagas
.
This is where you wait for actions to be triggered, run async processing logic, and send the results back through another action.
Using sagas complex async processing logic can be written in an elegant and linear fashion. Here's one example that updates the homepage slider component:
// scenes/homepage/slider/saga.js // we want to call the updateSlide action on the slider's logic storeconst actions = { const updateSlide = actions while true // wait for someone to call the updateSlide action or 5 seconds to pass const change timeout = // if timed out, advance the slide if timeout const currentSlide = sliderLogic // reset the clock to 5 seconds and wait again }
Read the documentation for redux-saga
to grasp their power!
Scenes
You can always treat the logic store reducers and sagas manually and plug them into your existing application.
If, however, you favor convenience, you may combine them into scenes.
Scenes are defined in scene.js
files like so:
// scenes/homepage/scene.js name: 'homepage' component: sceneComponent logic: sceneLogic sliderLogic sagas: sceneSaga sliderSaga
You may then access the combined scene like so:
homepageScenesaga === { ... } // start the scene sagas in parallelhomepageScene === {} // calls redux's combineReducers
or plug it into the kea-logic routing helpers.
Routes
Give redux-router
a helping hand:
// routes.js const scenes = homepage: todos: const routes = '/': 'homepage' '/todos': 'todos' '/todos/:visible': 'todos' scenes routes
... and have those scenes lazily loaded when route is accessed.
Code structure
While logic stores can exist anywhere, it is highly recommended to organise your code like this:
scenes
- a scene is a page or a subsystem in your appcomponents
- react components that are shared between scenesutils
- javascript utils shared between scenes
Side note: as we strive for simplicity, readability and clarity, we will use JavaScript Standard Style and skip semicolons. They are added/removed as needed in the transpiling/minimising stage, and add no value. Any "I forgot the semicolon" errors you might be worried about will be caught by the linter anyway. (Please install eslint and plugins for your IDE!)
Here's a typical structure:
scenes/homepage/
- index.js # the react component
- logic.js # actions, reducers, selectors
- saga.js # saga
- styles.scss # styles for this scene
- scene.js # one of these per scene
scenes/homepage/slider/
- _assets/ # some images
- index.js # the react component
- logic.js # actions, reducers, selectors
- saga.js # saga
- styles.scss # styles for the slider
scenes/todos/
- index.js # the react component
- logic.js # actions, reducers, selectors
- saga.js # saga
- styles.scss # styles for this scene
- scene.js # one of these per scene
scenes/todos/todo/
- index.js # the react component
components/
- header/
- index.js
- styles.scss
utils/
- create-uuid.js
- range.js
- delay.js
scenes/
- index.js
- routes.js
- styles.scss
/
- index.html
- index.js
- store.js
kea
?
What is All of this is a work in progress. Any and all help is greatly appreciated!
Read more here about the quest to find the smartest way to develop react applications.
List of public functions:
More documentation coming soon! Please help if you can!