Though you don't have to follow these conventions, you'll get off to a quicker start if you do, and your code will be more consistent and thus easier to maintain. Many of these conventions are largely an attempt to reuse common ideas and best practices that have been discovered by the Cycle community, and to provide additional abstractions to cover the specialised concerns of an isomorphic web application, and to promote the DRY principle.
Variables representing streams should be suffixed with $
, for example data$
. The variable name chosen, or a suffix thereof, should be a suitable name to represent the data emitted. For example, data$
should emit data
. Similarly, userAuthData$
is fine for emitting any of userAuthData
, authData
, or just data
. Example: authData$.map(data => ...)
or authData$.map(authData => ...)
var frog$ = most.of({ name: 'Hopper' })
frog$.observe(frog => console.log(`Hello, ${frog.name}`))
Higher-order streams should be represented with as many additional $
characters as is needed to represent the depth of streams that are nested. A stream of streams would be data$$
, and a stream of streams of streams would be data$$$
. If you know you're dealing with a higher-order stream, but the depth of stream nesting is an unknown or varies, just use $$
.
var frog$$ = most.from([
most.of({ name: 'Hopper' }),
most.of({ name: 'Chuzwazza' }),
most.of({ name: 'Kermit' })
])
frog$$.tap(frog$ => console.log('Got a new stream of frogs.'))
.join()
.observe(frog => console.log(`Hello, ${frog.name}`))
The word "components" refers to self-contained dataflow components. Though components are usually composed of multiple functions, the term "component function" refers to a given component's main function, which takes sources as inputs and returns a group of sinks.
- Component functions are named using
PascalCase
because, like classes (which we don't use), they represent what the component is, rather than what it does. All other functions are named usingcamelCase
. No function should useunderscore_casing
. - Component functions always take exactly one argument- a
sources
object. - Component functions always return an object with one or more sinks. Sinks are always streams.
- Before being exported from a module, component functions should always be wrapped with the
Component
function, found incommon/framework/component
. This provides automatic isolation and several other features, such as automatic logging and runtime identification of component boundaries for diagnostic purposes. - Internal functions which return a stream should be prefixed with
emit
, followed by a name representing the nature of the data to be emitted. Do not use the$
character for naming functions that return streams. - Internal functions which return a virtual DOM fragment should be named
view
if there is only one function generating a view for the component, or prefixed withview
when multiple functions exist for composing a component's virtual DOM.
Often a circular relationship exists between state-management components and UI components. The Component()
function provides a function proxy
, which allows a component's sinks to be constructed in advance without providing sources, so that the sinks can be used as inputs for other components with which a circular relationship exists. Component.fulfill()
can then be called afterward, passing a sources
object composed from the sink outputs of those components which depend on the originally-proxied component.
When making use of proxies, be careful not to inadvertently create infinite streams. A circular dependency should be thought of like a coil, rather than a circle- A direct circular relationship should not usually exist between a specific sink from one component and a specific source in the other component, unless it is very carefully managed. Instead, a sink from one component will typically feed a source in the other, which then flows to a secondary sink to be used as a secondary source in the first component. The "ends of the coil" would represent an original source input in the first component and the final driver output sink in one or the other component, with data flowing around the coil as it is transformed back and forth between the two (or more) components.
An example where a proxy might be used would be for a login page. An AuthState
component would manage a user's identity and authorization. A LoginPage
component would provide the login UI and expose an event$
stream indicating actions taken on the login page, particularly the login button event. The LoginPage
component would require the state$
sink from the AuthState
component in order to display login error messages. The AuthState
component would need to hook into a filtered subset of the event$
sink from the LoginPage
component in order to respond to login requests. Thus, a circular relationship exists in terms of component instantiation, but is not directly circular between the two components, as the actual circuit flows back through external drivers.
// A contrived example that merely demonstrates application of these conventions
function viewHeader(model) {
return h('header', [h('h1', [model.title])])
}
function viewMain(child) {
return h('main', [child])
}
function view(model, child) {
return h('div', [
viewHeader(model),
viewMain(child)
])
}
function emitView({model$, child}) {
return model$.map(model => view(model, child))
}
function Widget(sources) {
// Create a proxy for the FooState component. Sink name arguments can be omitted
// here if provided as an option when defining the component.
var foo = FooState.proxy(['state$', 'HTTP']);
var model$ = foo.state$.map(foo => ({title: foo.name}))
var child = ChildComponent({...sources, model$})
// The component instance is upgraded from proxy status once its sources have been
// fulfilled. If unfulfilled, it will be unable to emit any data through its sinks.
foo.fulfill({...sources, child.model$.map(model => model.xyz)})
return {
DOM: emitView(model$, child.DOM),
HTTP: most.merge(foo.HTTP, child.HTTP)
}
}
export default Component(Widget)
A state component is responsible for managing client-side business logic. Data that it processes should be exposed in a manner that is as agnostic as possible with respect to the way it is consumed. Think "pure" data, as opposed to data that exist soley for the benefit of a given user interface context.
It is tempting to offload state management to a driver, but to do so is to lose some of the key benefits provided by the Cycle pattern and to blur the lines between business logic and side effects. Drivers should be used purely as interfaces to the external world, and for sectioning off code for which significant side effects are unavoidable. Note that "side effects" refers to code that makes a permanent change to something external to itself. Private aggregation side effects for operators such as scan
, don't count.
- Primary state components will typically be instantiated at the root component level, with the exception being projected state that is only relevant within a particular branch of the user interface.
- A state component should avoid managing the full data of the application if possible. The set of data it manages should be restricted to that which is required by more than one child branch of the user interface. If a state component is required by only one child branch, consider moving the management of its lifecycle into that branch.
- A state component should generally return a single sink named
state$
, and emit ready-to-use state data that the component generates, along with any relevant driver sinks. - Use different state components for different areas of concern. When cross-over is required, provide one component's output as an input for the other (possibly employing proxies if required), or create a secondary state component for managing the intersection of the two areas of concern. Don't try to manage a huge all-encompassing graph of data in a single place, as you might do with Redux or Flux. Use of operators such as
scan
,combine
andzip
will be common within state components.
- Page components take a general sources object that is augmented with any additional state-management streams taken from state components, and return a sinks object containing standard driver sinks.
- A fully-composed HTML page is usually composed of a nested set of page components representing the "main" part of each matched child route. For example, the route
/admin/users/12
will likely be composed from four page components. The top-level component, matching the route/
will be the skeleton for all pages on the site. The next,/admin
, will be the subtemplate for the admin area and containg admin-specific navigation. The next,/users
, will be that of the user management section within the admin area. Finally,/12
will match the page for viewing a particular user within the user management section. The routing system is used to help select and compose page components correctly. - Important: Page components return a
page
sink rather than aDOM
sink. This allows the page to associate important metadata with the DOM representation, which then enables the server to return the correct HTTP status code for a particular initial request, as well as allowing the client to update the top-level page title correctly, and so forth. - The
page
sink is a stream that emits objects of{vtree, model, components, metadata}
. Helpers are provided that help you to assemble this object easily from multiple component sinks and streams of sinks.vtree
is the virtual DOM tree for that child routemodel
is the general data model representing the pagecomponents
represents the hierarchy of data models from which the page was composedmetadata
exposes relevant, standardized metadata that is expected for serving HTTP requests and updating HTML documents. Metadata is propagated and collapsed through the hierarchy of page components matched by the current route. Typically the component matched by the "leaf" route will provide much of this metadata, though defaults may be provided a parent when not specified by a child.status
is analogous to a page's HTTP statustitle
represents the documentTITLE
tagarea
aggregates a set of identifiers to be applied as CSS classes against theHTML
tag, allowing CSS to easily identify and style entire subsets of pages within a website's page hierarchy. A parent area might beadmin
, while a child area might beusers
. The classesarea--admin area--users
would then exist in theHTML
tag'sclassList
while the user is within that area.
- For pages that provide no functionality other than identifying an area within a site, use
PageAreaOnlyComponent
instead ofComponent
.
Note: The
components
property may yet be removed as it allows a parent to concern itself with a child's own dependencies, leading to more brittle code. In essence, it's also a violation of the Law of Demeter.
// TODO: page component example
UI components almost always take a props$
stream as a source. The props$
stream is analogous to a function signature; it expects data to be in a specific format, and it is the parent component's job to build the props$
stream correctly from incoming data, just as it is the job of anything calling a given function to know that function's signature and pass it correct arguments.
There are generally two kinds of user interface components; read-only and interactive.
Read-only components usually do not return a data model as a sink; their purpose is simply to represent incoming data correctly.
function ReadOnlyWidget({props$}) {
return {
DOM: props$.map(props => h('div', [`The value of this widget is ${props.value}`]))
}
}
export default Component(ReadOnlyWidget);
Interactive UI components usually expose their data model as a sink. A good technique for interactive components is to use two models. The first model is the primary model, which exposes the data represented by the component, and is what the component will return as a sink for external consumption. It will be constructed mostly from input events and the data-only outputs of any child components, but may take non-props sources as inputs in special cases as well. The primary model's purpose is to be a clean, externally-consumable representation of the user's inputs and interactions with the component's UI.
The second model is the internal model and is a transformed projection of the first model. It will contain a set of properties that will be passed to a view so that the view can be rendered without having to use any complex logic to figure out which elements to hide or show, or what their state should be. If a props$
stream is provided as an input to the component, it will usually be combined with the primary model to produce the internal model, rather thandirectly being used as an input to the primary model. In essence, input events and non-DOM driver are used to produce the primary model and other sinks. Props and the primary model are used to produce the internal model, and the internal model is used to produce the representation, i.e. the DOM.
It's very common to use higher-level components to compose other components into complex user interfaces. The DOM
driver allows us to construct virtual DOM trees from other DOM fragments directly, and/or from streams of fragments. Streams of fragments can themselves be higher-order streams, the children of which will be switched between automatically.
Use child DOM streams, rather than explicit DOM fragments, to create more efficient interfaces. If an encapsulating component has a view that rarely changes, but has several child DOM fragments that change frequently, rather than re-rendering the parent view every time a child's view changes, simply embed the child DOM stream directly into the parent DOM tree. The DOM driver will traverse the parent each time it is generated and automatically observe any child DOM streams embedded within the view, replacing those branches of the view with DOM fragments as they are emitted by the streams embedded there.