npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

powercycle

v0.20.5

Published

Composable Cycle.js with seamless React interop

Downloads

14

Readme

Powercycle is a transpositional extension for the wonderful framework Cycle.js. Transposition means that instead of only having views defined in streams, we're allowed to have streams defined in views as well! It powers up Cycle.js to the next level so it almost looks like a new framework! It puts the view in the center, to make composition as easy and trivial as it is in React - while keeping all the benefits of a purely functional-reactive environment. Any regular Cycle.js and React component can be included seamlessly in a Powercycle app.

function Timer(sources) {
  return (
    <h2>Timer: {xs.periodic(1000)}</h2>
  )
}

Basic examples:

App examples:

Motivation

React and Cycle.js have separate advantages and compromises, and I wanted to bring the good parts together.

Guide

  1. Installation
  2. Getting Started
  3. Static VDOM composition
  4. Streams and components everywhere
  5. Scopes
  6. Conditionals
  7. Collection
  8. Event props
  9. React realms
  10. Helpers, Shortcuts and Tips

Installation

Install powercycle and its peer dependencies:

npm install powercycle @cycle/react react xstream

Install the usual Cycle/react dependencies:

npm install @cycle/run @cycle/react-dom @cycle/state

Getting Started

JSX

Besides following the Installation steps, make sure that your setup can handle JSX, because Powercycle was made with JSX in mind. Powercycle has its own JSX pragma:

import withPower from 'powercycle'
/** @jsx withPower.pragma */
/** @jsxFrag withPower.Fragment */

Obviously you can skip using JSX, if you really wish, but you'll still need the pragma.

Static VDOM composition

We've seen the default import above named withPower, but let's forget that for now. Powercycle's core utility is the powercycle function, which takes 3 arguments, and returns a regular Cycle.js sinks object. (Don't pick on the name powercycle; at the end of the day, you won't even need to use this function.)

  1. The first argument is a static VDOM, which can contain streams and other components (even inline components! We'll see them later).
  2. The second argument is the sinks object, which contains all of the sinks for the current component, except the view.
  3. The third argument is the sources object which Powercycle will pass to the components during the VDOM traversal.

Let's see a basic example of an atomic component:

// Regular Cycle.js component
function Cmp(sources) {
  const state$ = sources.state.stream

  return {
    react: state$.map(state => <div>{state}</div>)
  }
}

It turns into:

function Cmp(sources) {
  const state$ = sources.state.stream

  return powercycle(
    <div>{state$}</div>,
    null,
    sources
  )
}

At the moment it doesn't seem to be much useful, but what happens when you want to include a child component, for example a Panel, in which you want to wrap the content. It leads to serious boilerplate:

function Cmp(sources) {
  const state$ = sources.state.stream

  const panelSinks = Panel({ ...sources, props: { title: 'State' }})

  return {
    react: panelSinks.react.map(panelVdom =>
      <div>
        State in a panel:
        {panelVdom}
      </div>
    )
  }
}

With Powercycle, it remains an atomic step:

function Cmp(sources) {
  const state$ = sources.state.stream

  return powercycle(
    <div>
      State in a panel:
      <Panel title="State">
        {state$}
      </Panel>
    </div>
    null,
    sources
  )
}

You can see the limitation in the first version: you can't put content and pass props to the Panel component in the VDOM. Wrapping a section of a content into a container is not a trivial action – you have to declare and manually invoke every related component separately from the VDOM. In the Powercycle example you might now have an idea how easy it can go with composition. The powercycle function will invoke the Panel component with passing the sources object to it (given as the 3rd parameter). Whenever the Panel component's view stream updates, the outer component will update as well. We'll see more powerful examples in the next sections, but let's not rush ahead.

How do we define our other sinks then, which are not the view? This is what the second argument is for:

function Cmp(sources) {
  const state$ = sources.state.stream

  return powercycle(
    <div>
      State in a panel:
      <Panel title="State">
        {state$}
      </Panel>
    </div>
    {
      state: ...,
      HTTP: ...
    },
    sources
  )
}

Let's wrap up what the powercycle function does exactly:

  1. It traverses the VDOM and searches for streams and components (see section Streams and components everywhere for details).
  2. It creates a view stream for the outer component which combines all the view streams in the given VDOM, and updates with the original VDOM structure.
  3. It collects all the non-view sink channels which were found in the inner components' sinks objects, and merges them all by channel. It also adds the sinks of the second argument to the merges. The result sinks object will be the return value of the powercycle function.

Shorthand return from components

From the last example of the previous section we learned that the VDOM traversal stops at the Panel component. The Panel component can do anything with the state$ stream which it received through sources.props.children. It can even dismiss it. This is the same behavior as you can find in React. In order to see the state in the app, the Panel component must include its sources.props.children in its VDOM:

function Panel(sources) {
  return powercycle(
    <div className="some panel styling">
      <h2 className="title">{sources.props.title}</h2>
      {sources.props.children}
    </div>,
    null,
    sources
  )
}

One important fact to realize here is that the Panel component is not invoked by the app developer, but by Powercycle. So Powercycle sees its output, and it can automatically call the powercycle function on it, so we have less to type:

function Panel(sources) {
  return [
    <div className="some panel styling">
      <h2 className="title">{sources.props.title}</h2>
      {sources.props.children}
    </div>,
    null,
    sources
  ]
}

This convenience shortcut changes the regular signature of a Cycle.js component's output, but we'll see how it hugely pays off. We can even omit the sources object too, because Powercycle already has it from the first powercycle call. If there's no non-view sink channel for the component, we can omit the second parameter too and the array wrapping, so this results in as simple as this:

function Panel(sources) {
  return (
    <div className="some panel styling">
      <h2 className="title">{sources.props.title}</h2>
      {sources.props.children}
    </div>
  )
}

withPower

Every component which returns with the shortcut return format, conveys the same amount of information as a regular Cycle.js component, it's just 'controlled' by Powercycle. The only thing we have to watch out for, is to have a root powercycle call to have the VDOM 'controlled'. It turns out, we can wrap our main component in a higher order function to do this:

import withPower from 'powercycle'
/** @jsx withPower.pragma */
/** @jsxFrag withPower.Fragment */

// ...

run(withPower(main), drivers)

With the withPower function, you don't ever need to use the powercycle function! You can just return with the VDOM, or with an array containing the VDOM and the event sinks object.

Streams and components everywhere

Powercycle collects streams and components from the VDOM according to the following rules:

  1. When it finds a stream as a VDOM child, it collects the stream:

    function main (sources) {
      // ...
      return (
        <div>{state$}</div>
      )
    }
  2. When it finds a stream in a prop of a plain DOM (e.g. a 'div') element, it collects the stream:

    function main (sources) {
      // ...
      return (
        <div style={ { background: color$ } }>...</div>
      )
    }
  3. When it finds a component (e.g. Panel) element, it invokes it with the sources objects, and collects its sinks. It doesn't continue the traversal under the component element. It passes the props object as sources.props. The inner component can access the children as sources.props.children:

    function Panel (sources) {
      return (
        <>
          <h1>{sources.props.title}</h1>
          {sources.props.children}
        </>
    }
    
    function main (sources) {
      return (
        <div>
          <Panel title="My Panel">...</Panel>
        </div>
      )
    }
  4. When it finds a function as a VDOM child, it's interpreted as an inline component. Powercycle will invoke the component with the sources object and collects its sinks, just like as it were a component element:

    function main (sources) {
      return (
        <div>
          {sources => {
            return [
              <div>...</div>,
              { state: ... }
            ]
          }}
        </div>
      )
    }

Scopes

Any VDOM node can have a scope prop, which will act as a regular Cycle.js isolation scope for the given element. As components act as boundaries in the Powercycle traversal, a scope will not just affect the component, but the complete sub-VDOM under it as well.

function ShowState(sources) {
  return (
    <pre>{sources.state.stream.map(JSON.stringify)}</pre>
  )
}

function main(sources) {
  const reducer$ = xs.of(() => ({
    foo: { bar: { baz: 5 } }
  }))

  return [
    <ShowState scope='foo' />,
    { state: reducer$ }
  ]
  // will show {"bar":{"baz":5}}"
}

Unlike the regular Cycle.js scope parameter, the scope prop can be a nested lens:

  return [
    <ShowState scope='foo.bar' />,
    { state: reducer$ }
  ]
  // will show {"baz":5}"

And of course it can be a full scope object:

  return [
    <ShowState scope={ { state: {
      get: state => JSON.stringify(state.foo.bar),
      set: (state, childState) => ({ ...state, foo: JSON.parse(childState)})
    } }} />
    { state: reducer$ }
  ]
  // will show "{\"baz\":5}"

The scope prop can be used on a DOM element as well. In this case, the scope will be applied to all the other props (for get and onChange, see Helpers, Shortcuts and Tips). If there's both an if and scope prop on the element, their precedence will be defined by their definition order on the node!

  // state: { todos: [{ text: 'todo1' }, { text: 'todo2' }, { text: 'todo3' }]}
  ...
  <Collection for='todos'>
    <div>
      <input scope='item.text' value={get()} onChange={({ target: { value } }) => () => value} />
      &nbsp;
      <button onClick={() => COLLECTION_DELETE}>Remove</button>
    </div>
  </Collection>

Automatic view scoping

By default, every component in Powercycle is scoped on the view channel. If you need to lift this rule occasionally, you can provide a noscope prop on the component. The reason for this rule is to make string VDOM selectors safe by default. String VDOM selectors are useful, because they eliminate the necessity of boilerplate Symbol declarations. Take a look at this inline component, which is inside a Collection item:

  {src => [
    <button sel='remove'>Remove</button>,
    { state: src.sel.remove.click.mapTo(COLLECTION_DELETE) }
  ]}

See the Todo example

Conditionals

If component:

Wraps the then or else value in a Fragment based on the cond property. The cond property can be either a stream or a stream callback. then and else values can any vdom child.

  <If cond={condition}
    then={value}
    else={otherValue}
  />

As an alternative way of defining the then branch, instead of using the then prop, you can define the then children as the vdom subtree of the If component:

  <If cond={condition} [else={...}]>
    {`then` value}
  />

if prop (applies to Component and DOM elements):

Controls the existence of the element based on the if condition:

  <div if={condition}>Remove</div>

If there's both an if and scope prop on the element, their precedence will be defined by their definition order on the node!

Collection

Powercycle has a Collection component which makes handling dynamic lists easy and trivial. By default, you don't need to provide any props to Collection. It uses the state channel as its input, so make sure that you scope down the state either on the Collection component or somewhere above. The Collection component will take its VDOM children as a fragment as its item component, so you can put anything between the opening and closing <Collection> tags. The Collection package also has a const for item removal reducer:

<Collection>
  <Combobox />

  {src => [
    <button sel='remove'>Remove</button>,
    { state: src.sel.remove.click.mapTo(COLLECTION_DELETE) }
  ]}
</Collection>

Reaching out to the outer state from the items

There are cases when you need to interact with the outer state from the items. For example, you need to duplicate an item, or set some state somewhere else, based upon the current item's state. For these cases, Collection automatically provides an extra stream in the items' sources object: outerState (the name can be specified with outerstate prop). Items can also leave reducers in their outerState sink. In order to interact with not just the collection array itself, even outer states, use the for prop on the Collection. The for prop works exactly like scope in specifying the collection's array, but it doesn't scope down the component, so outerState can see beyond the list.

/*
  {
    globalColor: "blue",
    foobar: {
      list: [{ color: "white", id: {} }, { color: "blue", id: {} }]
    }
  }
*/

<Collection for='foobar.list'>
  Set color: <Combobox />

  {src => [
    <button sel='set'>Set as global</button>,
    {
      outerState: src.sel.set.click
        .compose(sampleCombine(src.state.stream))
        .map(([click, state]) => outerState => ({
          ...outerState,
          globalColor: state.color
        }))
    }
  ]}
</Collection>

See the Todo app for an example

Event props

Powercycle makes use of onClick-style even props on elements. Event props are basically shortcuts for inline components. There are 2 types of event props:

  • on<Eventname>={ { sink1: <event$ to sink$ mapper>, sink2: <event$ to sink$ mapper>, ...} }

When the event prop value is an object, it is treated as a special sinks object, where the values are mappers between the event stream to the sink stream.

  • on<Eventname>={<event to state mapper>}

When the event prop value is a function, it is handled as a mapper between the event object and the state (reducer). The state will be consumed by the state sink.

Examples:

<div>
  {src => [
    Last click position: {get()}<br />
    <button>Make a request</button>,
    {
      state: src.el.click.map(ev => () => `${ev.clientX},${ev.clientY}`),
      HTTP: src.el.click.mapTo({ url: '?you-clicked' })
    }
  ]}
</div>

With using event props, this can be rewritten as below. You can see that there's no more need to wrap the fragment in an inline component:

<div>
  Last click position: {get()}<br />
  <button
    onClick={ {
      state: ev$ => ev$.map(ev => `${ev.clientX},${ev.clientY}`),
      HTTP: ev$ => ev$.mapTo({ url: '?you-clicked' })
    }}
  >Make a request</button>
</div>

Most of the times we're only concerned about the state sink. For this, the callback shortcut is even better:

<div>
  <button onClick={ev => () => ({ action: 'ADD' })}>Add</button>
</div>

Let's see another example. Here, we want to use a reducer with a <select> element:

function Combobox (sources) {
  return (
    <>
      <label>Color: </label>
      <select
        value={get('color')}
        onChange={ev => prev => ({ ...prev, color: ev.target.value })}
      >
        <option value='red'>Red</option>
        <option value='blue'>Blue</option>
      </select>
    </>
  )
}

In this case, by the time the reducer will be called, the event object will be nullyfied by React, and React will throw an error related to the synthetic event. Destructuring the arguments helps to overcome this problem:

function Combobox (sources) {
  return (
    <>
      <label>Color: </label>
      <select
        value={get('color')}
        onChange={({ target: { value }}) => prev => ({ ...prev, color: value })}
      >
        <option value='red'>Red</option>
        <option value='blue'>Blue</option>
      </select>
    </>
  )
}

React realms

React components can be included in the VDOM by wrapping them in the ReactRealm component. To use the state from the Cycle.js environment, Powercycle offers the useCycleState hook. You can put any content inside the opening and closing <ReactRealm> tags, they won't be traversed by Powercycle. That part of the VDOM will go directly into the React engine:

import { ReactRealm, useCycleState } from 'powercycle/util/ReactRealm'

function ReactCounter(props) {
  const [count, setCount] = useCycleState(props.sources)

  return (
    <div>
      <div>Counter: {count}</div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

function main(sources) {
  const state$ = sources.state.stream

  const reducer$ = xs.of(() => ({
    counter: 5
  }))

  return [
    <div>
      <ReactRealm scope='counter'>
        We're under a React realm!
        <ReactCounter />
      </ReactRealm>
      <pre>{state$.map(JSON.stringify)}</pre>
    </div>,
    { state: reducer$ }
  ]
}

Helpers, Shortcuts and Tips

  • Event props

  • map

    The map utility function is a handy helper to get the state in the VDOM. It has 2 signatures:

    • map(mapperFn, <sources>)

    If the sources object is provided as the second parameter, the map function returns with a stream which maps sources.state.stream over the mapperFn, so it's a shortcut for <sources>.state.stream.map(mapperFn):

    function ShowState(sources) {
      return (
        <Code>{map(JSON.stringify, sources)}</Code>
        {/* <Code>{sources.state.stream.map(JSON.stringify)}</Code> */}
      )
    }
    • map(mapperFn)

    If the sources object is omitted, then the map function returns with an inline component, which has the content of sources.state.stream, mapped over mapperFn, so it's a shortcut for { sources => <>{map(mapperFn, sources)}</> }:

    function ShowState(sources) {
      return (
        <Code>{map(JSON.stringify)}</Code>
        {/* <Code>{sources => <>{map(JSON.stringify, sources)}</>}</Code> */}
      )
    }

    Note, that in props, you can only use it with the sources object, as inline components are not applicable as props.

  • get

    The get function works exactly like map regarding its signature. The only difference is that it uses a Lodash getter as the mapperFn. It's a convenient shortcut for getting a chunk of the state:

    function ShowColor(sources) {
      const reducer$ = xs.of(() => ({
        color: 'red'
      }))
    
      return (
        <pre style={ { background: get('color', sources) } }>It's {get('color')}</pre>
      )
    }

    When the get function is called with no or empty parameter, it returns with the state object itself:

    function ShowColor(sources) {
      const reducer$ = xs.of(() => ({
        color: 'red'
      }))
    
      return (
        <Scope scope='color'>
          <pre style={ { background: get('', sources) } }>It's {get()}</Code>
        </Scope>
      )
    }
  • sources.sel[]

    View selection has a convenience shortcut. Instead of writing

    sources.react.select('input').events('change').map(ev => ev.target.value)

    You can write just:

    sources.sel.input.change['target.value']

    Example:

    <Collection>
      <pre>
        <Combobox />
    
        {src => [
          <button sel='remove'>Remove</button>,
          { state: src.sel.remove.click.mapTo(COLLECTION_DELETE) }
        ]}
    
        <br />
    
        {src =>
          <div style={ { color: get('color', src) } }>
            <ShowState />
          </div>
        }
      </pre>
    </Collection>
    • sources.el

    On a relative root element, you can even leave sel= and just write sources.el, which will refer to the root element:

    {src => [
      <button>Remove</button>,
      { state: src.el.click.mapTo(COLLECTION_DELETE) }
    ]}
  • How to opt-out from the Powercycle control?

    You can opt-out from Powercycle at any place in the VDOM by just returning a regular sinks object. The underlying components will not be controlled by Powercycle.