Modules
CreateApp itself doesn't do much work. It sets up a redux store, a couple of middlewares and returns a Stapp object. Without modules, application api will be empty, and its state will be a plain empty object. Like, forever.
So, what are the modules? A module is a place where all your magic should happen. A module has an ability to:
- create and handle portions of an app state;
- provide methods to a public application api;
- react to state changes;
- react to api calls.
A basic module is an object or a function, returning an object.
Definition
type Module<Api, State, FullState = State> = {
name: string
dependencies?: string[]
// Api
api?: Api
events?: Api // Alias for api
waitFor?: Array<
| AnyEventCreator
| string
| {
event: AnyEventCreator | string
timeout: number
}>
// State
state?: { [K: string]: Reducer<State[K]> }
reducers?: { [K: string]: Reducer<State[K]> } // alias for state
// Epics
epic?: Epic<Partial<Full>> | Array<Epic<Partial<Full>>>
epics?: Epic<Partial<Full>> | Array<Epic<Partial<Full>>> // alias for epic
}
Modules: handling state
Since Stapp uses redux as a state management core, reducers and events are the familiar redux concepts.
Reducer is a pure function, that receives an old state and an event, and must return a new state. An event (or an 'action' in redux terminology) is a plain object with only one required field type
. Stapp follows flux-standard-action convention.
Stapp provides some useful tools to simplify creating of reducers and event creators. These tools are inspired by redux-act, but have some tweaks and API differences to make the process even simpler.
Here is an example:
import { createEvent, createReducer } from 'stapp'
const setValue = createEvent('Set value')
const clearValue = createEvent('Clear value')
const setError = createEvent('Set errror')
const reset = createEvent('Reset state')
const valuesReducer = createReducer({})
.on(setValue, (values, newValues) => ({ ...values, ...newValues }))
.on(clearValue, (values, fieldName) => ({ ...values, [fieldName]: null }))
.reset(reset)
const errorsReducer = createReducer({})
.on(setError, (errors, newErrors) => ({ ...errros, ...newErrors }))
.reset(reset)
const formBase = {
name: FORM_BASE,
state: { // or `reducers`
values: valuesReducer,
errors: errorsReducer
}
}
A reducer created by createReducer is a function with some additional methods. Still, these reducers are just functions, so they can be combined into one with redux combineReducers
method. And remember, you are not forced to use createReducer at all.
Value of state
(or reducers
) fields of every module in the app will be merged into one object and combined into one root reducer, constructing the whole state of an app.
Modules: events
There are several ways to dispatch events to the store - each for its case.
If you are familiar with redux, you might ask: — Why do we call actions "events"? See, Flux and, specifically, redux borrowed much from CQRS and Event Sourcing patterns. Using the term "event" instead of "action" prompts to treat redux actions not only as commands to execute but also as events. Still, it's just an opinion, and you may use any tools compatible with flux-standard-action.
User interactions
If a module needs to handle some user interactions, it can add methods to the application's public API.
Any event creators passed through an api
field, will be bound to the store and exposed to an application consumer as API methods.
import { createEvent } from 'stapp'
export const formBase = {
name: FORM_BASE,
api: { // or `events`
setValue: createEvent('Set new values')
}
}
// somewhere later
<input name='example' onchange='event => api.setValue({
example: event.target.value
})' />
api
object can be nested:
import { createEvent } from 'stapp'
export const formBase = {
name: FORM_BASE,
api: { // or `events`
formBase: {
setValue: createEvent('Set new values'),
setError: createEvent('Set error')
}
}
}
// later
app.api.formBase.setValue(/* */)
app.api.formBase.setError(/* */)
Modules logic
All other events needed for business logic should not be exposed to the public API. Instead, they should be dispatched by so-called epics. The concept of epics will be explained below.
Testing
The application object created with createApp
has dispatch
and getState
methods. Although these methods can be used anywhere, the only reason to have them is that they are beneficial in unit tests. So please don't use them anywhere except tests.
Modules: epics
Epic is one the core concepts, which makes your application reactive. Epic can react to anything that happens with the application, and it should be the only place containing business-logic.
Epic is a function, that receives a stream of events and a stream of state, and must return a stream of events. The concept of epics is well explained here.
The only difference between redux-observable epics and Stapp epics is that the latter accepts a stream of a state as the last argument.
Here is an example of an epic:
import { map, filter } from 'rxjs/operators'
import { select, createEvent } from 'stapp'
import { setValue } from 'stapp-formbase'
const handleChange = createEvent('handle input change', event => ({
[event.target.name]: event.target.value
}))
const handleChangeEpic = (event$) => event$.pipe(
filter(select(handleChange)),
map(({ payload }) => setValue(payload))
)
const form = {
name: 'form',
api: { handleChange },
epic: handleChangeEpic
}
Module can provide a single epic or an array of epics.
See more about Epics in the Epics section.
Modules: module factories
A module factory is a function that returns a module. You'll find out at some point that most of your modules are module factories.
Sometimes your modules might have some common dependencies. E.g., a request service. Instead of passing them directly into a module, you should pass dependencies to the dependencies
field of createApp
config.
Every function passed to the modules
field will be called with a value provided to the dependencies
field.
// moduleA.js
import { select } from 'stapp'
import { switchMap, map, filter } from 'rxjs/operators'
import { request } from 'my-services'
const moduleA = ({ request }) => ({
epic: (event$) => event$.pipe(
filter(select(someEvent)),
switchMap(({ payload }) => request('/my-cool-api', payload)),
map(result => someOtherEvent(result))
)
})
// my-app.js
const app = createApp({
name: 'my app',
modules: [
moduleA
],
dependencies: {
request
}
})