As your application grows, a consistent structure becomes essential. This guide builds on the organization concepts introduced in the Organize guide and provides recommended patterns for large-scale applications.
src/
main/ // Root module
modules/ // Feature modules
feature1/ // Each feature is its own module
feature2/ // with the same structure
...
actions.js // Shared actions
factories.js // Custom action factories
sequences.js // Shared sequences
providers.js // Custom providers
computeds.js // Computed state values
reactions.js // Side effect handlers
errors.js // Custom error types
index.js // Main module definition
controller.js // App initialization
This structure organizes code primarily by type rather than by feature at the root level, with features encapsulated as modules. This approach balances cohesion and separation of concerns.
Each module (including the main module and feature modules) follows the same structure pattern:
The index file defines the module and imports all its components:
import { Module } from 'cerebral'
import * as sequences from './sequences'
import * as providers from './providers'
import * as reactions from './reactions'
import * as errors from './errors'
import featureA from './modules/featureA'
import featureB from './modules/featureB'
export default Module({
state: {
// Module state
isLoading: false
},
sequences,
providers,
signals: {}, // Legacy API, prefer sequences
reactions,
catch: [[errors.AuthError, sequences.handleAuthError]],
modules: {
featureA,
featureB
}
})
Actions are pure functions that do a specific job:
// Named function style
export function setUserData({ store, props }) {
store.set('users.current', props.user)
}
// Arrow function style
export const fetchUserData = ({ http, props }) =>
http
.get(`/api/users/${props.userId}`)
.then((response) => ({ user: response.data }))
Group related actions in the same file. If you have many actions, consider creating subdirectories by domain:
actions / auth.js // Authentication-related actions
users.js // User-related actions
posts.js // Post-related actions
Sequences combine actions and factories to create logical flows:
import { set, when } from 'cerebral/factories'
import { state, props } from 'cerebral'
import * as actions from './actions'
export const fetchUser = [
set(state`users.isLoading`, true),
actions.fetchUserData,
set(state`users.current`, props`user`),
set(state`users.isLoading`, false)
]
export const loginUser = [
set(state`auth.isAuthenticating`, true),
actions.authenticateUser,
when(props`success`),
{
true: [set(state`auth.isLoggedIn`, true), fetchUser],
false: [set(state`auth.error`, props`error`)]
},
set(state`auth.isAuthenticating`, false)
]
For complex applications, consider organizing sequences by feature domain similar to actions.
Custom factories create reusable, configurable actions:
// Debounce input changes
export const debounceInput = (time) => {
return function debounceInput({ debounce, props, path }) {
return debounce
.shared('inputChange', time)
.then(path.continue)
.catch(path.discard)
}
}
// Mark form fields as touched
export const touchField = (field) => {
return function touchField({ store }) {
store.set(`form.fields.${field}.touched`, true)
}
}
Define custom providers that expose services to your actions:
export const api = {
get(url) {
return fetch(url).then((response) => response.json())
},
post(url, data) {
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then((response) => response.json())
}
}
export const logger = (context) => {
context.logger = {
info(message) {
console.log(`[INFO] ${message}`)
},
error(message, error) {
console.error(`[ERROR] ${message}`, error)
}
}
return context
}
Reactions respond to state changes with side effects:
export const saveUserSettingsToLocalStorage = ({ watch, get, controller }) => {
// Trigger when user settings change
watch(state`user.settings.**`, () => {
const settings = get(state`user.settings`)
localStorage.setItem('userSettings', JSON.stringify(settings))
})
// Ensure reaction is triggered once during initialization
controller.on('initialized', () => {
const storedSettings = localStorage.getItem('userSettings')
if (storedSettings) {
controller.getSequence('user.initializeSettings')({
settings: JSON.parse(storedSettings)
})
}
})
}
Custom error types for better error handling:
import { CerebralError } from 'cerebral'
export class AuthenticationError extends CerebralError {
constructor(message, details) {
super(message, details)
this.name = 'AuthenticationError'
}
}
export class ValidationError extends CerebralError {
constructor(message, fields) {
super(message, { fields })
this.name = 'ValidationError'
}
}
Modules can access each other’s state but should avoid directly calling sequences from other modules. Instead:
// Parent module sequence
export const userSelectedPost = [
set(state`posts.currentId`, props`postId`),
set(state`posts.showDetails`, true),
...when(state`users.isLoaded`),
{
true: [],
false: [
// Load user data needed by posts module
actions.loadUsers
]
}
]
As your application grows:
Here’s an example of a complete e-commerce feature module structure:
src /
main /
modules /
ecommerce /
modules /
cart / // Shopping cart submodule
catalog / // Product catalog submodule
checkout / // Checkout process submodule
actions.js // Shared e-commerce actions
sequences.js // Shared e-commerce sequences
providers.js // E-commerce specific API client
computeds.js // E-commerce derived state
errors.js // E-commerce specific errors
index.js // E-commerce module definition
This structure allows the e-commerce module to be self-contained while enabling interaction with other application features like authentication or user profiles.