cycle-utils
utilities and higher-order components for transforming Cycle components, sources and sinks
why
The popular utility library Recompose allows you to write simple React components and independently augment them with additional granular and testable layers of additional functionality.
This library provides simple higher-order component factories and other utilities to simplify Cycle.js components and allow you to write your own higher-order component libraries easily.
installation
npm install --save @sunny-g/cycle-utils
usage
import { mapSources } from '@sunny-g/cycle-utils';
// or
import mapSources from '@sunny-g/cycle-utils/es2015/mapSources';
api
A higher-order component (HOC) is a function that takes in a Cycle.js component and returns a Cycle.js component.
This pattern makes HOCs composable and allows us to use Ramda's compose
or pipe
functions to stitch multiple HOCs together into a single, larger HOC.
The following HOCs and utilities are provided by this library:
- Higher-order components:
- Utilities:
mapSources()
mapSources(
sourceNames,
sourceMapper
): HigherOrderComponent
HOC that applies the sourceMapper
to sources
before they've been passed into the BaseComponent
.
parameters:
sourceNames: '*' | string | string[]
: Sources you want to transform ('*'
if you want to pass the entiresources
object)sourceMapper: (Sources | ...Sources[]) => Sources
: Transform function to be applied to specifiedsources
; the returnedSources
are merged into the originalSources
Example:
// adds a fetched property to the `props` object provided by a fictional `props` source
const withNewProps = mapSources(
[ 'props', 'HTTP' ], (propsSource, HTTP) => {
const newProp = HTTP
.select('newProp')
.flatten();
const newPropsSource = combine(propsSource, newProp)
.map(([ props, newProp ]) => ({
...props,
'newProp': newProp,
});
return { props: newPropsSource };
}
);
const ComponentWithProps = withNewProps(Component);
mapSinks()
mapSinks(
sinkNames,
sinkMapper
): HigherOrderComponent
HOC that applies the sinkMapper
to sinks
after they've been returned from the BaseComponent
.
parameters:
sinkNames: '*' | string | string[]
: Sources you want to transform ('*'
if you want to pass the entiresinks
object)sinkMapper: (Sinks | ...Sinks[]) => Sinks
: Transform function to be applied to specifiedsinks
; the returnedSinks
are merged into the originalSinks
Example:
// logs all emitted HTTP requests
const logHTTPSink = mapSinks(
'HTTP', (HTTPSink) => ({
HTTP: HTTPSink.debug('making an HTTP request'),
})
);
const ComponentWithLoggedHTTPSink = logHTTPSink(Component);
mapSinksWithSources()
mapSourcesAndSink(
sinkNames,
sourceNames,
sinkAndSourceMapper
): HigherOrderComponent
HOC to transform a component's sinks
with any desired sources
after the sinks
have been returned from the BaseComponent
.
parameters:
sinkNames: '*' | string | string[]
: Sinks you want to transform ('*'
if you want the entiresinks
object)sourceNames: '*' | string | string[]
: Sources you want to transform ('*'
if you want the entiresources
object)sinkAndSourceMapper: (Sinks | ...Sinks[], Sources | ...Sources[]) => Sinks
: Transform function to be applied to specifiedsinks
andsources
; the returnedSinks
are merged into the originalSinks
Example:
// logs all emitted sinks values with the current props
const logAllSinks = mapSinksWithSources(
'*', 'props', (sinks, propsSource) => {
return Object.keys(sinks)
.reduce((newSinks, sinkName) => ({
...newSinks,
[sinkName]: sinks[sinkName]
.compose(sampleCombine(propsSource))
.debug('new sink emission with current props')
}), {});
}
);
const ComponentWithLoggedSinksWithProps = logAllSinks(Component);
mapSourcesAndSinks()
mapSourcesAndSink(
sourceNames,
sourceMapper,
sinkNames,
sinkMapper
): HigherOrderComponent
HOC to transform both a component's sources
before entering the BaseComponent
and a component's sinks
after they've been returned from the BaseComponent
.
Uses mapSources
and mapSinksWithSources
under the hood, so the same requirements of those functions apply.
parameters:
sourceNames: '*' | string | string[]
: Sources you want to transform ('*'
if you want the entiresources
object)sourceMapper: (Sources | ...Sources[]) => Sources
: Transform function to be applied to specifiedsources
; the returnedSources
are merged into the originalSources
sinkNames: '*' | string | string[]
: Sources you want to transform ('*'
if you want the entiresinks
object)sinkMapper: (Sinks | ...Sinks[], Sources) => Sinks
: Transform function to be applied to specifiedsinks
as well as the entiresources
object; the returnedSinks
are merged into the originalSinks
Example:
// TODO: ADD AN EXAMPLE HERE
isolate()
isolate(
config: ((Sources: any) => null | string | {}) | null | string | {}
): HigherOrderComponent
HOC version of @cycle/isolate
.
parameters:
config: ((Sources: any) => null | string | {}) | null | string | {}
: Eithernull
, astring
, or anobject
, or a function that is givensources
and returnsnull
, astring
, or anobject
Example:
// any component wrapped with randomIsolation will receive a randomly-generated scope
const randomIsolation = isolate(() => Math.random().toString());
const NewComponent = randomIsolation(Component);
combineSinks()
combineSinks(
combiners: { [sinkName: string]: sinkCombiner }
): SinksCombiner
Utility to declaratively combine multiple Sinks
objects
parameters:
combiners: { [sinkName: string]: sinkCombiner }
: Object ofsinkCombiner
s for eachsinkName
to combine- each individual
sinkCombiner
has the signature(...sink) => sink
and is given eachSink
from each passed-inSinks
object and should return a combinedSink
stream - NOTE: if the
sinkCombiner
for a givensinkName
is missing and there are multiplesinks
of thatsinkName
, thesink
's nativemerge
function is applied to the list ofsinks
- each individual
returns:
SinksCombiner: (...Sinks[]) => Sinks
: A function to apply to multipleSinks
objects that:- groups all
Sink
s of the samesinkName
into an array - applies each individual
sinkCombiner
to the destructured array of theSink
s - creates and returns a new
Sinks
object from the combinedSink
s.
- groups all
Example:
// say we have multiple non-identical children component sinks
// as well as the component's own sinks...
// has only a DOM sink
const mainSinks = { DOM: ... };
// each has both DOM and HTTP sinks
const childOneSinks = childOne(sources);
const childTwoSinks = childTwo(sources);
const sinkCombiner = combineSinks({
// some drivers only require a merge of their sinks
// combineSinks merges by default, but is shown here
// note that there is no placeholder argument for missing sinks (mainSinks has no HTTP sink)
HTTP: (childOneHTTP, childTwoHTTP) => xs.merge(childOneHTTP, childTwoHTTP),
// some sinks require custom merging/combining...
DOM: (mainDOM$, childOneDOM$, childTwoDOM$) => xs
.combine(mainDOM$, childOneDOM$, childTwoDOM$)
.map(([ mainDOM, childOneDOM, childTwoDOM ]) =>
div([
mainDOM,
div([
childOneDOM,
childTwoDOM,
])
])
),
});
return sinkCombiner(mainSinks, childOneSinks, childTwoSinks);
combineCycles()
combineCycles(
combiners: { [sinkName: string]: sinkCombiner },
...BaseComponents: Component[]
): CombinedComponent
Utility to declaratively combine multiple Cycle components into a single Cycle component
parameters:
combiners: { [sinkName: string]: sinkCombiner }
: Object ofsinkCombiner
s for eachsinkName
to combine- each individual
sinkCombiner
has the signature(...sinks) => sink
and is given eachSink
from each passed-inSinks
object and should return a combinedSink
stream - NOTE: if the
sinkCombiner
for a givensinkName
is missing and there are multiplesinks
of thatsinkName
, the firstsink
's nativemerge
operator is applied to the list ofsinks
- each individual
...BaseComponents: Component[]
: The desired Cycle.js components to combine into a single component
returns:
CombinedComponent: (Sources | ...Sources[]) => Sinks
: An otherwise normal Cycle.js component that differs from traditional components in that it takes in either:- a single
Sources
object to be passed into eachBaseComponent
- an list of
Sources
objects, each one passed into the correspondingBaseComponent
by index
- a single
Example:
// assume the same `childOne` and `childTwo` components from the previous example
const ChildrenComponent = combineCycles({
HTTP: (...httpSinks) => xs.merge(...httpSinks),
DOM: (childOneDOM$, childTwoDOM$) => xs
.combine(childOneDOM$, childTwoDOM$)
.map(([ childOneDOM, childTwoDOM ]) =>
div([
childOneDOM,
childTwoDOM,
])
),
}, childOne, childTwo);
const childrenSinks = ChildrenComponent(sources);
Another useful example is for chaining tasks together in a more linear format:
// the first task, makes a request
const task1 = _ => ({
HTTP: of({
url: 'https://google.com',
category: 'req1',
}),
});
// the second task
// maps the response of the first task's request into the response sink of the second task
const task2 = ({ HTTP }) => ({
HTTP: HTTP
.select('req1')
.flatten()
.map(res1 => ({
url: res1.url,
category: 'req2'
})),
});
function main(sources) {
const Task = combineCycles({}, task1, task2);
const taskSinks = Task(sources);
// now, we can use the result of the tasks
const taskRes = sources.HTTP
.select('req2')
.flatten();
// ... the rest of the component, returns main sinks merged with taskSinks...
}
contributing
todo
- ensure typings are comprehensive and correct
- explain contribution process
- add more tests :)
- explain why I wrote this
license
ISC