Cerebral
A state controller with its own debugger
The Cerebral Webpage is now launched
You can access the webpage at http://christianalfoni.com/cerebral/
How to get started
1. Install debugger
Install the Chrome Cerebral Debugger
2. Choose a package
Cerebral is the controller layer of your application. You will also need a model layer to store your state and a view layer to produce your UI.
An example would be:
npm install cerebral && npm install cerebral-react && npm install cerebral-immutable-store
The following packages are currently available:
Model packages
The API you use in Cerebral to change state is the same for all packages, but read their notes to see their differences.
cerebral-immutable-store by @christianalfoni. An immutable state store with the possibility to define state that maps to other state. This package also supports recording
cerebral-baobab by @Yomguithereal. An immutable state store which allows you to use facets to map state. This package does not currently support recording and uses the BETA version of Baobab V2
cerebral-immutable-js by @facebook (Coming soon). Immutable state with very high performance, but lacks the possibility to map state. Does support recording
View packages
cerebral-react by @facebook. An application wrapper component, mixin, decorators and HOC. Pure render is built in. Can also be used with react-native
cerebral-angular by @angular. A provider for using Cerebral
Deprecated packages
As Cerebral now allows you to choose the model layer and view layer separately these packages are deprecated:
3. Get started
Lets look at an example. Each model layer repo has a detailed description of how to create a controller, and each view layer repo has information on how to use the controller to produce UI and change state.
import Controller from 'cerebral';
import Model from 'cerebral-model-package';
import request from 'superagent';
const state = {
foo: 'bar'
};
const services = {
request: request
};
const model = Model(state);
export default Controller(model, services)
4. Signals and actions
To create a signal please read the README of the chosen package. To define a signals action chain, please read on. This is the same for all packages.
Naming
The way you think of signals is that something happened in your application. Either in your VIEW layer, a router, maybe a websocket connection etc. So the name of a signal should define what happened: "appMounted", "inputChanged", "formSubmitted". The actions are named by their purpose, like "setInputValue", "postForm" etc. This will make it very easy for you to read and understand the flow of the application. All signal definitions first tells you "what happened in your app". Then each action describes its part of the flow that occurs when the signal triggers.
Action
The convention is to create each action as its own module. This will keep your project clean and let you easily extend actions with type checks and other options. It is important to name your functions as that will make it easier to read debugging information.
function myAction () {
};
export default myAction;
Arguments
function MyAction (input, state, output, services) {
input
state.set('isLoading', false);
state.unset('isLoading');
state.merge('user', {name: 'foo'});
state.push('list', 'foo');
state.unshift('list', 'bar');
state.pop('list');
state.shift('list');
state.concat('list', [1, 2, 3]);
state.splice('list', 1, 1, [1]);
state.push(['admin', 'users'], {foo: 'bar'});
state.get('foo');
state.get(['foo', 'bar']);
output({foo: 'bar'});
output.success({foo: 'bar'});
output.error({foo: 'bar'});
services
};
export default MyAction;
Note: Asynchronous actions cannot mutate state. Calling set
or merge
on the state
parameter above will throw an error.
It is best practice not to mutate state in async actions.
Chain
actions/setLoading.js
function setLoading (input, state) {
state.set('isLoading', true);
};
export default setLoading;
actions/setTitle.js
function setTitle (input, state) {
state.set('title', 'Welcome!');
};
export default setTitle;
main.js
import controller from './controller.js';
import setLoading from './actions/setLoading.js';
import setTitle from './actions/setTitle.js';
controller.signal('appMounted',
setLoading,
setTitle
);
Trigger
controller.signal('appMounted',
setLoading,
setTitle
);
controller.signals.appMounted();
controller.signals.appMounted({
foo: 'bar'
});
controller.signals.appMounted(true, {
foo: 'bar'
});
Paths
Paths allows you to conditionally run actions depending on the result of the previous action. This is typically useful with asynchronous actions, but you can use them next to any action you run. The default paths are success
and error
, but you can define custom paths if you need to.
main.js
import controller from './controller.js';
import checkSomething from './actions/checkSomething.js';
import setSuccessMessage from './actions/setSuccessMessage.js';
import setErrorMessage from './actions/setErrorMessage.js';
controller.signal('appMounted',
checkSomething, {
success: [setSuccessMessage],
error: [setErrorMessage]
}
);
Async
Async actions are defined like normal actions, only inside an array.
main.js
import controller from './controller.js';
import loadUser from './actions/loadUser.js';
import setUser from './actions/setUser.js';
import setError from './actions/setError.js';
controller.signal('appMounted',
[
loadUser, {
success: [setUser],
error: [setError]
}
]
);
When defining multiple actions in an array, they will run async in parallel and their outputs will run after all initial async actions are done.
main.js
import controller from './controller.js';
import loadUser from './actions/loadUser.js';
import setUser from './actions/setUser.js';
import setUserError from './actions/setUserError.js';
import loadProjects from './actions/loadProjects.js';
import setProjects from './actions/setProjects.js';
import setProjectsError from './actions/setProjectsError.js';
controller.signal('appMounted',
[
loadUser, {
success: [setUser],
error: [setUserError]
},
loadProjects, {
success: [setProjects],
error: [setProjectsError]
}
]
);
Services
You can inject any services you need to talk to the server, do complex computations etc. These services are injected when instantiating the controller.
function getUser(input, state, output, services) {
services.ajax.get('/user').then(output.success);
}
You can also use ES6 syntax:
function getUser(input, state, output, {ajax}) {
ajax.get('/user').then(output.success);
}
Outputs
You can define custom outputs. This will override the default "success" and "error" outputs. What is especially nice with manually defining outputs is that they will be analyzed by Cerebral. You will get errors if you use your actions wrong, are missing paths for your outputs etc.
function myAction (input, state, output) {
if (state.get('isCool')) {
output.foo();
} else if (state.get('isAwesome')) {
output.bar();
} else {
output();
}
};
myAction.defaultOutput = 'foo';
myAction.outputs = ['foo', 'bar'];
export default myAction;
Types
You can type check the inputs and outputs of an action to be notified when you are using your signals the wrong way. The default type checking with Cerebral is very simple. If you know type checking well it is encouraged to use the "custom type checks" instead. But is type checking new to you, this is a good place to start.
function myAction (input, state, output) {
output({foo: 'bar'});
};
myAction.input = {
isCool: String
};
myAction.output = {
foo: String
};
myAction.outputs = {
success: {
result: Object
},
error: {
message: String
}
};
myAction.output = undefined;
myAction.outputs = {
foo: null,
bar: undefined
};
export default myAction;
The following types are available: String, Number, Boolean, Object, Array, null and in addition undefined where you do not want to pass a value.
Custom Type Checking
You can use a function instead. That allows you to use any typechecker. We will use tcomb in this example.
import t from 'tcomb';
function myAction (input, state, output) {
output({foo: 'bar'});
};
myAction.input = {
foo: t.String.is,
bar: t.maybe(t.Number).is,
signature: function (value) {
return typeof value === 'string';
}
};
Groups
By using ES6 syntax you can easily create groups of actions that can be reused.
const MyGroup = [Action1, Action2, Action3];
controller.signal('appMounted', Action4, ...MyGroup);
controller.signal('appMounted', Action5, ...MyGroup, Action6);
Events
controller.on('change', function () {});
controller.on('error', function (error) {});
controller.on('signalStart', function () {});
controller.on('signalEnd', function () {});
controller.on('actionStart', function (isAsync) {});
controller.on('actionEnd', function () {});
Functional Traits
Since actions are pure functions it is very easy for you to compose functions and even action chains together. You might have a complex flow that is to be reused across signals.
Simple action
function myAction (input, state, output, services) {}
controller.signal('appMounted', myAction);
Action Factory
function actionFactory (someArg) {
return function myCustomAction (input, state, output, services) {}
}
controller.signal('appMounted', actionFactory('someArg'), someOtherAction);
Now the custom action has access to the argument passed to the factory. This is especially great for handling http requests where maybe only the url differs on the different requests.
Action Chain Factory
function actionChainFactory (someArg) {
return [actionFactory(someArg), actionFactoryB('someOtherArg'), actionC];
}
controller.signal('appMounted', ...actionChainFactory('someArg'), someOtherAction);
By returning an array from a factory you are able to do some pretty nifty compositions. By using the ES6 spread operator you can easily inject this chain into any part of the signal chain.
How to create a custom Cerebral VIEW package
view packages in Cerebral just uses an instantiated Cerebral controller to get state, do state changes and listen to state changes. The package you create basically just needs an instance of a Cerebral controller and you will have access to the following information.
function myCustomViewPackage (controller) {
controller.get(path);
controller.on('change', function () {
});
controller.on('remember', function () {
});
};
That is basically all need to update the view layer.
How to create a custom Cerebral MODEL package
In this example we will use the immutable-store project as a model.
index.js
var Store = require('immutable-store');
var getValue = function (path, obj) {
path = path.slice();
while (path.length) {
obj = obj[path.shift()];
}
return obj;
};
module.exports = function (state) {
return function (controller) {
var initialState = Store(state);
state = initialState;
controller.on('reset', function () {
state = initialState;
});
controller.on('seek', function (seek, isPlaying, recording) {
state = state.import(recording.initialState);
});
return {
get: function (path) {
return pathToValue(path, state);
},
toJSON: function () {
return state.toJS();
},
getInitialRecordingState: function () {
return state.export();
},
mutators: {
set: function (path, value) {
var key = path.pop();
state = getValue(path, state).set(key, value);
}
}
};
};
};
Demos
TodoMVC: www.christianalfoni.com/todomvc
Cerebral - The beginning
Read this article introducing Cerebral: Cerebral developer preview
Contributors
- Marc Macleod: Discussions and code contributions
- Petter Stenberg Hansen: Logo and illustrations
- Jesse Wood: Article review
Thanks guys!