UNPKG

@cycle/react

Version:

Utilities to interoperate between Cycle.js and React

63 lines 12.3 kB
{ "name": "@cycle/react", "version": "2.10.0", "description": "Utilities to interoperate between Cycle.js and React", "author": "Andre Staltz <contact@staltz.com>", "license": "MIT", "bugs": "https://github.com/cyclejs/react/issues", "homepage": "https://github.com/cyclejs/react", "repository": "https://github.com/cyclejs/react/tree/master", "keywords": [ "react", "cyclejs", "xstream", "mvi", "react-native", "driver" ], "main": "lib/cjs/index.js", "typings": "lib/cjs/index.d.ts", "types": "lib/cjs/index.d.ts", "files": [ "lib/cjs/*" ], "prettier": { "singleQuote": true, "trailingComma": "es5", "bracketSpacing": false }, "peerDependencies": { "@cycle/run": "5.x.x", "react": ">=16.0", "xstream": "11.x.x" }, "devDependencies": { "@cycle/isolate": "^5.2.0", "@cycle/run": "^5.5.0", "@types/mocha": "^9.0.0", "@types/node": "14.x", "@types/react": "17.0.38", "c8": "^7.11.0", "mocha": "^9.1.3", "prettier": "^2.1.2", "react": "17.0.2", "react-dom": "17.0.2", "react-test-renderer": "17.0.2", "symbol-observable": "^1.2.0", "ts-node": "^10.4.0", "typescript": "4.5.4", "xstream": "11.14.0" }, "publishConfig": { "access": "public" }, "scripts": { "format": "prettier --write ./{src,test}/**/*.{ts,tsx,js}", "compile": "npm run compile-cjs && npm run compile-es6", "compile-cjs": "tsc --module commonjs --outDir ./lib/cjs", "compile-es6": "echo 'TODO' : tsc --module es6 --outDir ./lib/es6", "coverage": "c8 --reporter=lcov npm test", "test": "mocha test/*.ts --require ts-node/register --recursive" }, "readme": "# Cycle React\n\n> Interoperability layer between Cycle.js and React\n\n- Use React (DOM or Native) as the rendering library in a Cycle.js app\n- Convert a Cycle.js app into a React component\n- Support model-view-intent architecture with isolation scopes\n\n```\nnpm install @cycle/react\n```\n\n## Example\n\n```js\nimport xs from 'xstream';\nimport {render} from 'react-dom';\nimport {h, makeComponent} from '@cycle/react';\n\nfunction main(sources) {\n const inc = Symbol();\n const inc$ = sources.react.select(inc).events('click');\n\n const count$ = inc$.fold(count => count + 1, 0);\n\n const vdom$ = count$.map(i =>\n h('div', [\n h('h1', `Counter: ${i}`),\n h('button', {sel: inc}, 'Increment'),\n ]),\n );\n\n return {\n react: vdom$,\n };\n}\n\nconst App = makeComponent(main);\n\nrender(h(App), document.getElementById('app'));\n```\n\nOther examples:\n\n- [Use React inside Cycle.js (CodeSandbox)](https://codesandbox.io/s/4zqply47nw)\n- [Use Cycle.js to write a React component (CodeSandbox)](https://codesandbox.io/s/6xzrv29963)\n\nRead also the [announcement blog post](https://staltz.com/use-react-in-cyclejs-and-vice-versa.html).\n\n## Usage\n\n<details>\n <summary><strong>Installation</strong> (click here)</summary>\n <p>\n\nInstall the package:\n\n```bash\nnpm install @cycle/react\n```\n\nNote that this package **only supports React 16.4.0** and above. Also, as usual with Cycle.js apps, you might need `xstream` (or another stream library).\n\n</p>\n</details>\n\n<details>\n <summary><strong>Use React as the rendering library</strong> (click here)</summary>\n <p>\n\nUse the hyperscript `h` function (from this library) to create streams of ReactElements:\n\n```js\nimport xs from 'xstream'\nimport {h} from '@cycle/react'\n\nfunction main(sources) {\n const vdom$ = xs.periodic(1000).map(i =>\n h('div', [\n h('h1', `Hello ${i + 1} times`)\n ])\n );\n\n return {\n react: vdom$,\n }\n}\n```\n\nAlternatively, you can also use JSX or `createElement`:\n\n```jsx\nimport xs from 'xstream'\n\nfunction main(sources) {\n const vdom$ = xs.periodic(1000).map(i =>\n <div>\n <h1>Hello ${i + 1} times</h1>\n </div>\n );\n\n return {\n react: vdom$,\n }\n}\n```\n\nHowever, to attach event listeners in model-view-intent style, you must use `h` which supports the special prop `sel`. See the next section.\n\n </p>\n</details>\n\n<details>\n <summary><strong>Listen to events in the Intent</strong> (click here)</summary>\n <p>\n\nUse hyperscript `h` and pass a **`sel`** as a prop. `sel` means \"selector\" and it's special like `ref` and `key` are: it does not affect the rendered DOM elements. Then, use that selector in `sources.react.select(_).events(_)`:\n\n```js\nimport xs from 'xstream'\nimport {h} from '@cycle/react'\n\nfunction main(sources) {\n const increment$ = sources.react.select('inc').events('click')\n\n const count$ = increment$.fold(count => count + 1, 0)\n\n const vdom$ = count$.map(x =>\n h('div', [\n h('h1', `Counter: ${x}`),\n h('button', {sel: 'inc'}),\n ])\n )\n\n return {\n react: vdom$,\n }\n}\n```\n\nThe `sel` can be a string or a symbol. We recommend using symbols to avoid string typos and have safer guarantees when using multiple selectors in your Cycle.js app.\n\n </p>\n</details>\n\n<details>\n <summary><strong>Pass event handlers as props to react components</strong> (click here)</summary>\n <p>\n\nUse hyperscript `h` and pass a **`sel`** as a prop. Use that selector in `sources.react.select(sel).events(whatever)` to have cyclejs/react pass an `onWhatever` function to the react component:\n\n```js\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { makeComponent, h } from \"@cycle/react\";\n\n// React component\nfunction Welcome(props) {\n return (\n <div>\n <h1>Hello, {props.name}</h1>\n <button onClick={() => props.onPressWelcomeButton({ random: Math.random().toFixed(2) }) } >\n press me\n </button>\n </div>\n );\n}\n\n// Cycle.js component that uses the React component above\nfunction main(sources) {\n const click$ = sources.react\n .select('welcome')\n .events('pressWelcomeButton')\n .debug('btn')\n .startWith(null);\n\n const vdom$ = click$.map(click =>\n h('div', [\n h(Welcome, { sel: 'welcome', name: 'madame' }),\n h('h3', [`button click event stream: ${click}`])\n ])\n );\n\n return {\n react: vdom$\n };\n}\n\nconst Component = makeComponent(main);\nReactDOM.render(<Component />, document.getElementById('root'));\n```\n\n </p>\n</details>\n\n<details>\n <summary><strong>Isolate event selection in a scope</strong> (click here)</summary>\n <p>\n\nThis library supports isolation with `@cycle/isolate`, so that you can prevent components from `select`ing into each other even if they use the same string `sel`. Selectors just need to be unique within an isolation scope.\n\n```js\nimport xs from 'xstream'\nimport isolate from '@cycle/isolate'\nimport {h} from '@cycle/react'\n\nfunction child(sources) {\n const elem$ = xs.of(\n h('h1', {sel: 'foo'}, 'click$ will NOT select this')\n )\n return { react: vdom$ }\n}\n\nfunction parent(sources) {\n const childSinks = isolate(child, 'childScope')(sources)\n\n const click$ = sources.react.select('foo').events('click')\n\n const elem$ = childSinks.react.map(childElem =>\n h('div', [\n childElem,\n h('h1', {sel: 'foo'}, `click$ will select this`),\n ])\n )\n\n return { react: elem$ }\n}\n```\n\n </p>\n</details>\n\n<details>\n <summary><strong>(Easy) Convert a Cycle.js app into a React component</strong> (click here)</summary>\n <p>\n\nUse `makeComponent` which takes the Cycle.js `main` function and a `drivers` object and returns a React component.\n\n```js\nconst CycleApp = makeComponent(main, {\n HTTP: makeHTTPDriver(),\n history: makeHistoryDriver(),\n});\n```\n\nThen you can use `CycleApp` in a larger React app, e.g. in JSX `<CycleApp/>`. Any props that you pass to this component will be available as `sources.react.props()` which returns a stream of props.\n\nIf you are not using any other drivers, then you do not need to pass the second argument:\n\n```js\nconst CycleApp = makeComponent(main);\n```\n\n </p>\n</details>\n\n<details>\n <summary>(Advanced) Convert a Cycle.js app into a React component (click here)</summary>\n <p>\n\nBesides `makeComponent`, this library also provides the `makeCycleReactComponent(run)` API which is more powerful and can support more use cases.\n\nIt takes one argument, a `run` function which should set up and execute your application, and return three things: source, sink, (optionally:) events object, and dispose function.\n\n- `run: () => {source, sink, events, dispose}`\n\nAs an example usage:\n\n```js\nconst CycleApp = makeCycleReactComponent(() => {\n const reactDriver = (sink) => new ReactSource();\n const program = setup(main, {...drivers, react: reactDriver});\n const source = program.sources.react;\n const sink = program.sinks.react;\n const events = {...program.sinks};\n delete events.react;\n for (let name in events) if (name in drivers) delete events[name];\n const dispose = program.run();\n return {source, sink, events, dispose};\n});\n```\n\n**source** is an instance of ReactSource from this library, provided to the `main` so that events can be selected in the intent.\n\n**sink** is the stream of ReactElements your `main` creates, which should be rendered in the component we're creating.\n\n**events** is a *subset* of the sinks, and contains streams that describe events that can be listened by the parent component of the `CycleApp` component. For instance, the stream `events.save` will emit events that the parent component can listen by passing the prop `onSave` to `CycleApp` component. This `events` object is optional, you do not need to create it if this component does not bubble events up to the parent.\n\n**dispose** is a function `() => void` that runs any other disposal logic you want to happen on componentWillUnmount. This is optional.\n\nUse this API to customize how instances of the returned component will use shared resources like non-rendering drivers. See recipes below.\n\n </p>\n</details>\n\n<details>\n <summary>Recipe: from main and drivers to a React component (click here)</summary>\n <p>\n\nUse the shortcut API `makeComponent` which is implemented in terms of the more the powerful `makeCycleReactComponent` API:\n\n```js\nimport {setup} from '@cycle/run';\n\nfunction makeComponent(main, drivers, channel = 'react') {\n return makeCycleReactComponent(() => {\n const program = setup(main, {...drivers, [channel]: () => new ReactSource()});\n const source = program.sources[channel];\n const sink = program.sinks[channel];\n const events = {...program.sinks};\n delete events[channel];\n for (let name in events) if (name in drivers) delete events[name];\n const dispose = program.run();\n return {source, sink, dispose};\n });\n}\n```\n\n </p>\n</details>\n\n<details>\n <summary>Recipe: from main and engine to a React component (click here)</summary>\n <p>\n\nAssuming you have an `engine` created with `setupReusable` (from `@cycle/run`), use the `makeCycleReactComponent` API like below:\n\n```js\nfunction makeComponentReusing(main, engine, channel = 'react') {\n return makeCycleReactComponent(() => {\n const source = new ReactSource();\n const sources = {...engine.sources, [channel]: source};\n const sinks = main(sources);\n const sink = sinks[channel];\n const events = {...sinks};\n delete events[channel];\n const dispose = engine.run(sinks);\n return {source, sink, dispose};\n });\n}\n```\n\n </p>\n</details>\n\n<details>\n <summary>Recipe: from source and sink to a React component (click here)</summary>\n <p>\n\nUse the `makeCycleReactComponent` API like below:\n\n```js\nfunction fromSourceSink(source, sink) {\n return makeCycleReactComponent(() => ({source, sink}));\n}\n```\n\n </p>\n</details>\n\n<details>\n <summary><strong>Make a driver that uses ReactDOM</strong> (click here)</summary>\n <p>\n\nSee [`@cycle/react-dom`](https://github.com/cyclejs/react-dom).\n\n </p>\n</details>\n\n<details>\n <summary><strong>Make a driver that uses React Native</strong> (click here)</summary>\n <p>\n\nSee [`@cycle/react-native`](https://github.com/cyclejs/react-native).\n\n </p>\n</details>\n\n## License\n\nMIT, Copyright Andre 'Staltz' Medeiros 2018\n" }