@pqmcgill/cycle-react
v1.0.8
Published
A Cycle.js driver that treats React virtual DOM trees as sinks and pipes React events via sources
Downloads
13
Readme
cycle-react
Cycle React is a Cycle.js driver that renders React virtual dom elements via sinks, and emits React events via sources.
It is heavily inspired by @cycle/dom, and was built in an effort to provide the wonderful API of the DOM driver while simultaneously providing access to the rich ecosystem of React components.
Using Cycle React allows you to create hybrid Cycle/React applications while providing the best aspects of both worlds.
Installation
These instructions assume that you already have an existing running Cycle.js application. If you don't, then follow this excellent documentation to get started: Cycle.js Getting Started
Cycle React depends on your application already including two packages: React and React-Dom. Both at >= version 16.3
# installing peer dependencies
npm install --save [email protected]^ [email protected]^
To install Cycle React itself is simply
# installing cycle-react
npm install --save @pqmcgill/cycle-react
You should now be set to use Cycle React
Usage
Once you have all of the above packages installed, usage is quite simple.
Setup
First you will want to import the function makeReactDriver
from cycle-react. It takes either a document query selector or dom element as input, and returns a driver that renders your app into that element.
import { run } from '@cycle/run';
import { makeReactDriver } from '@pqmcgill/cycle-react';
import Main from './Main';
const drivers = {
React: makeReactDriver('#app')
};
run(Main, drivers);
hyperscript h()
Cycle React provides a hyperscript function called h()
that has the exact same signature as React.createElement
. It takes three arguments: tagName
, props
, and children
, and returns a ReactElement
.
h(tagName: string, props: Object, children...: Array<string | number | ReactElement>): ReactElement
The reason for the h()
function is to process the props object passed into it prior to rendering in order to allow Cycle React to understand how to handle events as we'll see a little further down.
Using the h()
function, we can create a stream of React VDom nodes.
import xs from 'xstream';
import { h } from '@pqmcgill/cycle-react';
function Main(sources) {
return {
React: xs.of(
h('div', {},
h('p', {}, 'Follow this link to learn more about Cycle.js'),
h('a', { href: 'https://cycle.js.org/' }, 'Cycle.js')
)
)
};
}
The h()
function will also render existing React
Components
import xs from 'xstream';
import { h } from '@pqmcgill/cycle-react';
import MyReactComponent from './MyReactComponent';
function Main(sources) {
return {
React: xs.of(
h(MyReactComponent, { foo: 'bar' })
)
};
}
Most developers don't like working with the h()
functions directly, which is why Cycle React offers hyperscript helper functions to make the code more legible.
import xs from 'xstream';
import { div, a } from '@pqmcgill/cycle-react';
function Main(sources) {
return {
React: xs.of(
div([
p('Follow this link to learn more about Cycle.js'),
a({ href: 'https://cycle.js.org/' }, 'Cycle.js')
])
)
};
}
You can even use jsx
! Just point your jsx configuration to use the h()
function.
Babel
/** @jsx/h */
import { h } from '@pqmcgill/cycle-react';
function Main(sources) {
...
const view$ = xs.of(
<div>
<p>Sweet! I can use JSX!!!</p>
<a href="https://cycle.js.org/">Seriously! Check out these docs</a>
</div>
);
...
}
Typescript (.tsconfig.json)
{
"compilerOptions": {
...,
"jsx": "react",
"jsxFactory": "h"
}
}
Events
How Cycle React handles events is what makes this project unique. Normally, in a React project, you would make use of an imperative api for handling events. For example:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
clicked: false
};
}
this.handleClick = (e) => {
// imperatively do something with event
this.setState(() => ({
clicked: true
}))
}
render() {
return (
<div>
{ this.state.clicked && <p>Clicked!</p> }
<button onClick={ this.handleClick }>Click!</button>
</div>
)
}
}
In Cycle React, you would write the same code in a more declarative style:
import { h } from '@pqmcgill/cycle-react';
function MyComponent(sources) {
// declaratively subscribe to events
const click$ = sources.React
.select('myBtn')
.event('click')
.map(e => true)
.startWith(false);
const view$ = click$.map(clicked => (
<div>
{ clicked && <p>Clicked!</p> }
<button selector="myBtn">Click!</button>
</div>
));
return {
React: view$
};
}
Notice the use of the selector
prop. This prop is special and allows Cycle React to wire up the sources properly for subscribing to events. React Cycle provides a React object on the sources map. The React source has a public method select(selector: string)
that will return an instance of ReactSource
. ReactSource
has a public method event(eventType: string): Stream<any>
. The returned Stream from calling event(eventType)
emits values when the corresponding prop named on[EventType]
is called on the component with the selector
prop. This provides an excellent means of interoperability between pure React Components and Cycle.js apps.
The above code works because button
has a prop named onClick
. onClick
is a built-in prop, but we're not limited to built-in props. Take the following example which uses an existing React Component with a props based callback API.
class Timer extends React.Component {
...
componentDidMount() {
let count = 0;
setTimeout(() => {
this.props.onTick(count++);
}, 1000);
}
...
}
function Main(sources) {
const tick$ = sources
.select('timer')
.event('tick')
.subscribe({
next(v) { console.log(v); }
});
return {
React: xs.of(
<Timer selector="timer" />
)
};
}
Isolation
When using @cycle/isolate
to provide a scope to a component,
isolate(MyComponent, 'scoped')(sources)
any cycle-react events that the component subscribes to will also be scoped to that component. This mitigates the risk for namespace collisions when using the selector prop, and allows for the same component to be reused multiple times on the same page.