Application development is about handling events to run side effects and produce changes to the state. Cerebral uses sequences to define these flows in a declarative, functional way.
Let’s compare a traditional imperative approach to Cerebral’s declarative sequence approach.
import { App } from 'cerebral'
const app = App({
state: {
title: 'My Project',
posts: [],
users: {},
userModal: {
show: false,
id: null
},
isLoadingPosts: false,
isLoadingUser: false,
error: null
},
methods: {
openPostsPage() {
this.state.isLoadingPosts = true
this.providers.api.getPosts().then((posts) => {
this.state.posts = posts
this.state.isLoadingPosts = false
})
},
openUserModal(id) {
this.state.isLoadingUser = true
this.providers.api.getUser(id).then((user) => {
this.state.users[id] = user
this.state.userModal.id = id
this.state.isLoadingUser = false
})
}
},
providers: {
api: {
/*...*/
}
}
})
In Cerebral, we think about what should happen before we define how it happens. Let’s define our sequences first:
import { App } from 'cerebral'
import { set } from 'cerebral/factories'
import { state, props } from 'cerebral'
const app = App({
state: {
title: 'My Project',
posts: [],
users: {},
userModal: {
show: false,
id: null
},
isLoadingPosts: false,
isLoadingUser: false,
error: null
},
sequences: {
openPostsPage: [
set(state`isLoadingPosts`, true),
getPosts,
set(state`posts`, props`posts`),
set(state`isLoadingPosts`, false)
],
openUserModal: [
set(state`userModal.id`, props`id`),
set(state`userModal.show`, true),
set(state`isLoadingUser`, true),
getUser,
set(state`users.${props`id`}`, props`user`),
set(state`isLoadingUser`, false)
]
},
providers: {
api: {
/*...*/
}
}
})
Now we need to implement the actions used in our sequences:
// Action to get posts
function getPosts({ api }) {
return api.getPosts().then((posts) => ({ posts }))
}
// Action to get user
function getUser({ api, props }) {
return api.getUser(props.id).then((user) => ({ user }))
}
Every function in a sequence is called an action. Actions receive a context object with access to:
Actions can be synchronous or asynchronous:
// Synchronous action
function syncAction({ store }) {
store.set('some.path', 'some value')
}
// Asynchronous action with Promise
function asyncAction({ api }) {
return api.getSomething().then((data) => ({ data }))
}
// Asynchronous action with async/await
async function modernAction({ api }) {
const data = await api.getSomething()
return { data }
}
When a sequence triggers, you can pass it an object of props. These props are available to all actions in the sequence:
// Trigger a sequence with props
app.getSequence('openUserModal')({ id: 123 })
When an action returns an object, its properties are merged with the existing props and passed to the next action:
;[
// Props: { id: 123 }
function firstAction({ props }) {
return { foo: 'bar' } // Adds to props
},
// Props: { id: 123, foo: 'bar' }
function secondAction({ props }) {
console.log(props) // { id: 123, foo: 'bar' }
}
]
Cerebral includes factories - functions that create actions. They help you write more concise and readable code:
import { set, push, toggle, when } from 'cerebral/factories'
import { state, props } from 'cerebral'
export const mySequence = [
// Set a value in state
set(state`user.name`, props`name`),
// Push to an array
push(state`items`, props`newItem`),
// Toggle a boolean
toggle(state`menu.isOpen`),
// Conditional logic
when(state`user.isAdmin`),
{
true: [set(state`adminTools.visible`, true)],
false: [set(state`adminTools.visible`, false)]
}
]
You can also use object notation (like state.user.name
) with the babel-plugin-cerebral in your project.
This approach makes your sequences more declarative and easier to understand.
You can visualize sequence execution with the Cerebral debugger. To try it out, add this to your entry file:
// Get a reference to a sequence
const openPostsPage = app.getSequence('openPostsPage')
// Run it
openPostsPage()
When you refresh your application, you should see the sequence execution in the debugger. Experiment with the checkboxes at the top of the execution window to adjust the level of detail shown.
Sequences provide a structured way to define your application logic by:
In the next sections, we’ll explore more advanced features like branching paths and error handling.