Sequence

Sequences are the primary way to define logic flows in Cerebral. They allow you to compose actions in a declarative way.

Basic Sequences∞ 

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
}

Async Actions∞ 

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
]

Sequence Factory∞ 

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
])

Composition∞ 

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
]

Parallel∞ 

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∞ 

Paths allow you to create branches in your sequences based on the result of an action.

Basic Path Usage∞ 

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()
  }
}

Passing Data to Paths∞ 

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)
    })
  }
}

Async Path Selection∞ 

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]
  }
]

Status-Based Paths∞ 

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]
  }
]

Optional Paths∞ 

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]
  }
]

Nesting Paths∞ 

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]
  }
]

Running Sequences∞ 

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 @cerebral/babel-plugin.