Sequences are the primary way to define logic flows in Cerebral. They allow you to compose actions in a declarative way.
Sequences can be expressed with a simple array:
export const mySequence = [actions.doSomething, actions.doSomethingElse]
You attach sequences to modules:
import * as sequences from './sequences'
export default {
sequences
}
By default sequences run synchronously, but an action might run asynchronously, making the sequence async. When an action returns a promise, the sequence waits for it to resolve before continuing:
function myAsyncAction() {
return Promise.resolve({ result: 'data' })
}
// Or using async/await
async function myAsyncAction() {
const data = await fetchSomething()
return { result: data }
}
export const mySequence = [
myAsyncAction, // Sequence will wait for this to resolve
actions.useTheResult
]
While using array literals is the simplest approach, you can also be explicit by using the sequence factory:
import { sequence } from 'cerebral/factories'
import * as actions from './actions'
// Unnamed sequence
export const mySequence = sequence([actions.someAction, actions.anotherAction])
// Named sequence (useful for debugging)
export const namedSequence = sequence('My Important Flow', [
actions.someAction,
actions.anotherAction
])
Sequences can be composed within other sequences:
import * as actions from './actions'
// Define a reusable sequence
export const authenticateSequence = [
actions.validateCredentials,
actions.requestToken,
actions.storeToken
]
// Use it within another sequence
export const loginSequence = [
actions.showLoadingIndicator,
...authenticateSequence,
actions.redirectToDashboard,
actions.hideLoadingIndicator
]
// Or
export const loginSequence = [
actions.showLoadingIndicator,
authenticateSequence,
actions.redirectToDashboard,
actions.hideLoadingIndicator
]
While JavaScript is single-threaded, Cerebral can run multiple asynchronous actions concurrently using parallel
:
import { parallel } from 'cerebral/factories'
import * as actions from './actions'
export const loadDataSequence = [
actions.setLoading,
parallel([actions.loadUsers, actions.loadPosts, actions.loadSettings]),
actions.unsetLoading
]
// Named parallel for debugging
export const loadDataSequence = [
actions.setLoading,
parallel('Load Application Data', [
actions.loadUsers,
actions.loadPosts,
actions.loadSettings
]),
actions.unsetLoading
]
The sequence continues only when all parallel actions have completed.
Paths allow you to create branches in your sequences based on the result of an action.
import * as actions from './actions'
export const submitForm = [
actions.validateForm,
{
valid: [actions.submitForm, actions.showSuccessMessage],
invalid: [actions.showValidationErrors]
}
]
The action before the paths object decides which path to take:
function validateForm({ path }) {
const isValid = /* validation logic */
if (isValid) {
return path.valid()
} else {
return path.invalid()
}
}
You can pass data when taking a path:
function validateForm({ path, props }) {
if (props.form.isValid) {
return path.valid({
validatedData: props.form.data
})
} else {
return path.invalid({
validationErrors: getErrors(props.form)
})
}
}
Paths work with promises too:
function checkUserPermission({ api, path, props }) {
return api
.checkPermission(props.userId)
.then((response) => {
if (response.hasPermission) {
return path.allowed({ permissions: response.permissions })
} else {
return path.denied({ reason: response.reason })
}
})
.catch((error) => path.error({ error }))
}
export const userSequence = [
actions.checkUserPermission,
{
allowed: [actions.grantAccess],
denied: [actions.redirectToUnauthorized],
error: [actions.showError]
}
]
You can create paths for specific scenarios like HTTP status codes:
function getUser({ http, path, props }) {
return http
.get(`/users/${props.id}`)
.then((response) => path.success({ user: response.data }))
.catch((error) => {
if (error.status === 404) {
return path.notFound()
} else {
return path.error({ error })
}
})
}
export const loadUser = [
actions.getUser,
{
success: [actions.setUser],
notFound: [actions.redirectToUserNotFound],
error: [actions.showErrorMessage]
}
]
Not all defined paths need to be used. Actions can choose which paths to include:
// The action might take any of these paths, but isn't required to use all
export const userSequence = [
actions.processUser,
{
admin: [actions.loadAdminTools],
regular: [actions.loadRegularDashboard],
guest: [actions.redirectToLogin],
error: [actions.showError]
}
]
Paths can be nested to create complex conditional flows:
export const checkoutSequence = [
actions.validateCart,
{
valid: [
actions.processPayment,
{
success: [actions.createOrder, actions.showReceipt],
declined: [actions.showPaymentError],
error: [actions.logPaymentError]
}
],
invalid: [actions.showCartError]
}
]
There are several ways to run sequences:
import { sequences, state } from 'cerebral'
// From an action
function myAction({ get, props }) {
// Get a sequence and run it
const mySequence = get(sequences`mySequence`)
mySequence({ someData: props.data })
}
// From a component
connect(
{
buttonClicked: sequences`mySequence`
},
({ buttonClicked }) => {
return <button onClick={() => buttonClicked({ id: 123 })}>Click me</button>
}
)
// From a reaction
Reaction(
{
isLoggedIn: state`user.isLoggedIn`
},
({ isLoggedIn, get }) => {
if (isLoggedIn) {
get(sequences`loadDashboard`)()
}
}
)
You can also use object notation (like sequences.mySequence
) with the babel-plugin-cerebral.