├── .npmignore
├── .eslintrc
├── examples
├── simple
│ ├── public
│ │ ├── favicon.ico
│ │ └── index.html
│ ├── src
│ │ ├── index.js
│ │ └── Timer.js
│ └── package.json
└── redux-todos
│ ├── src
│ ├── reducers
│ │ ├── index.js
│ │ ├── visibilityFilter.js
│ │ ├── todos.js
│ │ └── todos.spec.js
│ ├── components
│ │ ├── App.js
│ │ ├── Footer.js
│ │ ├── Todo.js
│ │ ├── Link.js
│ │ └── TodoList.js
│ ├── actions
│ │ ├── index.js
│ │ └── index.spec.js
│ ├── index.js
│ └── containers
│ │ ├── FilterLink.js
│ │ ├── VisibleTodoList.js
│ │ └── AddTodo.js
│ ├── .gitignore
│ ├── package.json
│ ├── public
│ └── index.html
│ └── README.md
├── .babelrc
├── src
├── forceArray.js
├── shallowClone.js
├── forceArray.test.js
├── customRxOperators.js
├── shallowClone.test.js
├── updateNodeStreams.js
├── registerListeners.js
├── index.js
├── getNodeSelectors.js
├── customCreateElement.js
└── component.js
├── .gitignore
├── tests
└── index.js
├── package.json
├── README.md
└── index.d.ts
/.npmignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["standard", "standard-react"]
3 | }
--------------------------------------------------------------------------------
/examples/simple/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/recyclejs/recycle/HEAD/examples/simple/public/favicon.ico
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": [
4 | ["transform-react-jsx"],
5 | "transform-object-rest-spread"
6 | ]
7 | }
--------------------------------------------------------------------------------
/src/forceArray.js:
--------------------------------------------------------------------------------
1 | function forceArray (arr) {
2 | if (!Array.isArray(arr)) return [arr]
3 | return arr
4 | }
5 |
6 | export default forceArray
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated code
2 | /index.js
3 | /lib
4 | /dist
5 | /coverage
6 |
7 | node_modules
8 | .DS_Store
9 | npm-debug.log
10 | _book
11 | package-lock.json
--------------------------------------------------------------------------------
/examples/simple/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import Timer from './Timer'
4 |
5 | render( , document.getElementById('root'))
6 |
7 |
--------------------------------------------------------------------------------
/src/shallowClone.js:
--------------------------------------------------------------------------------
1 | function shallowClone (data) {
2 | if (Array.isArray(data)) {
3 | return [...data]
4 | } else if (typeof data === 'object') {
5 | return {...data}
6 | }
7 | return data
8 | }
9 |
10 | export default shallowClone
11 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 | import visibilityFilter from './visibilityFilter'
4 |
5 | const todoApp = combineReducers({
6 | todos,
7 | visibilityFilter
8 | })
9 |
10 | export default todoApp
11 |
--------------------------------------------------------------------------------
/src/forceArray.test.js:
--------------------------------------------------------------------------------
1 | /* global expect, it */
2 | import forceArray from './forceArray'
3 |
4 | it('should return an array', () => {
5 | expect(forceArray(1)).toBeInstanceOf(Array)
6 | })
7 |
8 | it('should return an array', () => {
9 | expect(forceArray([1, 2, 3])).toBeInstanceOf(Array)
10 | })
11 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/reducers/visibilityFilter.js:
--------------------------------------------------------------------------------
1 | const visibilityFilter = (state = 'SHOW_ALL', action) => {
2 | switch (action.type) {
3 | case 'SET_VISIBILITY_FILTER':
4 | return action.filter
5 | default:
6 | return state
7 | }
8 | }
9 |
10 | export default visibilityFilter
11 |
--------------------------------------------------------------------------------
/examples/redux-todos/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
19 |
--------------------------------------------------------------------------------
/src/customRxOperators.js:
--------------------------------------------------------------------------------
1 | export const reducer = (reducerFn) => (stream) =>
2 | stream.map(event => ({ reducer: reducerFn, event }))
3 |
4 | export default Rx => {
5 | if (Rx && Rx.Observable && !Rx.Observable.prototype.reducer) {
6 | Rx.Observable.prototype.reducer = function (reducerFn) {
7 | return reducer(reducerFn)(this)
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Footer from './Footer'
3 | import AddTodo from '../containers/AddTodo'
4 | import VisibleTodoList from '../containers/VisibleTodoList'
5 |
6 | const App = () => (
7 |
12 | )
13 |
14 | export default App
15 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/actions/index.js:
--------------------------------------------------------------------------------
1 | let nextTodoId = 0
2 | export const addTodo = (text) => ({
3 | type: 'ADD_TODO',
4 | id: nextTodoId++,
5 | text
6 | })
7 |
8 | export const setVisibilityFilter = (filter) => ({
9 | type: 'SET_VISIBILITY_FILTER',
10 | filter
11 | })
12 |
13 | export const toggleTodo = (id) => ({
14 | type: 'TOGGLE_TODO',
15 | id
16 | })
17 |
--------------------------------------------------------------------------------
/src/shallowClone.test.js:
--------------------------------------------------------------------------------
1 | /* global expect, it */
2 | import shallowClone from './shallowClone'
3 |
4 | it('should create shallow clone', () => {
5 | let a = {
6 | firstLevel: 1
7 | }
8 | let b = {
9 | firstLevel: 2,
10 | second: a
11 | }
12 | let c = shallowClone(b)
13 |
14 | expect(c).not.toBe(b)
15 | expect(c.second).toBe(b.second)
16 | })
17 |
18 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import { createStore } from 'redux'
4 | import { Provider } from 'react-redux'
5 | import App from './components/App'
6 | import reducer from './reducers'
7 | import 'rxjs/add/operator/withLatestFrom'
8 |
9 | const store = createStore(reducer)
10 |
11 | render(
12 |
13 |
14 | ,
15 | document.getElementById('root')
16 | )
17 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FilterLink from '../containers/FilterLink'
3 |
4 | const Footer = () => (
5 |
6 | Show:
7 | {" "}
8 |
9 | All
10 |
11 | {", "}
12 |
13 | Active
14 |
15 | {", "}
16 |
17 | Completed
18 |
19 |
20 | )
21 |
22 | export default Footer
23 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Todo = ({ onClick, completed, text }) => (
5 |
11 | {text}
12 |
13 | )
14 |
15 | Todo.propTypes = {
16 | onClick: PropTypes.func.isRequired,
17 | completed: PropTypes.bool.isRequired,
18 | text: PropTypes.string.isRequired
19 | }
20 |
21 | export default Todo
22 |
--------------------------------------------------------------------------------
/examples/simple/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "examples",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "react-scripts": "0.9.5"
7 | },
8 | "scripts": {
9 | "start": "react-scripts start",
10 | "build": "react-scripts build",
11 | "test": "react-scripts test --env=jsdom",
12 | "eject": "react-scripts eject"
13 | },
14 | "dependencies": {
15 | "react": "^16.1.1",
16 | "react-dom": "^16.1.1",
17 | "recycle": "^3.0.0",
18 | "rxjs": "^5.5.2"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/updateNodeStreams.js:
--------------------------------------------------------------------------------
1 | export default Rx => function updateNodeStreams (listeners, nodeStreams) {
2 | listeners.forEach(regRef => {
3 | const streams = nodeStreams
4 | .filter(ref => ref.selector === regRef.selector)
5 | .filter(ref => ref.selectorType === regRef.selectorType)
6 | .filter(ref => ref.event === regRef.event)
7 | .map(ref => ref.stream)
8 |
9 | if (streams.length) {
10 | regRef.stream.next((streams.length === 1) ? streams[0] : Rx.Observable.merge(...streams))
11 | }
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/src/registerListeners.js:
--------------------------------------------------------------------------------
1 | const registerListeners = Rx => (listeners, selectorType) => selector => ({
2 | addListener: event => {
3 | let ref = listeners
4 | .filter(ref => ref.selector === selector)
5 | .filter(ref => ref.selectorType === selectorType)
6 | .filter(ref => ref.event === event)[0]
7 |
8 | if (!ref) {
9 | ref = {
10 | selector,
11 | selectorType,
12 | event,
13 | stream: new Rx.Subject()
14 | }
15 | listeners.push(ref)
16 | }
17 |
18 | return ref.stream.switch()
19 | }
20 | })
21 |
22 | export default registerListeners
23 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/components/Link.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Link = ({ active, children, onClick }) => {
5 | if (active) {
6 | return {children}
7 | }
8 |
9 | return (
10 | {
12 | e.preventDefault()
13 | onClick()
14 | }}
15 | >
16 | {children}
17 |
18 | )
19 | }
20 |
21 | Link.propTypes = {
22 | active: PropTypes.bool.isRequired,
23 | children: PropTypes.node.isRequired,
24 | onClick: PropTypes.func.isRequired
25 | }
26 |
27 | export default Link
28 |
--------------------------------------------------------------------------------
/examples/redux-todos/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todos",
3 | "version": "0.0.1",
4 | "private": true,
5 | "devDependencies": {
6 | "enzyme": "^2.4.1",
7 | "react-addons-test-utils": "^15.3.0",
8 | "react-scripts": "^0.9.3"
9 | },
10 | "scripts": {
11 | "start": "react-scripts start",
12 | "build": "react-scripts build",
13 | "eject": "react-scripts eject",
14 | "test": "react-scripts test"
15 | },
16 | "dependencies": {
17 | "prop-types": "^15.6.0",
18 | "react": "^16.1.1",
19 | "react-dom": "^16.1.1",
20 | "react-redux": "^5.0.6",
21 | "recycle": "^3.0.0",
22 | "redux": "^3.7.2"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import component from './component'
3 | import { Subject } from 'rxjs/Subject'
4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'
5 | import { Observable } from 'rxjs/Observable'
6 | import 'rxjs/add/observable/merge'
7 | import 'rxjs/add/operator/map'
8 | import 'rxjs/add/operator/mapTo'
9 | import 'rxjs/add/operator/do'
10 | import 'rxjs/add/operator/filter'
11 | import 'rxjs/add/operator/switch'
12 |
13 | const Rx = {
14 | Subject,
15 | Observable,
16 | BehaviorSubject
17 | }
18 |
19 | export const recycle = component(React, Rx)
20 | export { reducer } from './customRxOperators'
21 | export default recycle
22 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/components/TodoList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Todo from './Todo'
4 |
5 | const TodoList = ({ todos, onTodoClick }) => (
6 |
7 | {todos.map(todo =>
8 | onTodoClick(todo.id)}
12 | />
13 | )}
14 |
15 | )
16 |
17 | TodoList.propTypes = {
18 | todos: PropTypes.arrayOf(PropTypes.shape({
19 | id: PropTypes.number.isRequired,
20 | completed: PropTypes.bool.isRequired,
21 | text: PropTypes.string.isRequired
22 | }).isRequired).isRequired,
23 | onTodoClick: PropTypes.func.isRequired
24 | }
25 |
26 | export default TodoList
27 |
--------------------------------------------------------------------------------
/examples/redux-todos/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Redux Todos Example
7 |
8 |
9 |
10 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/actions/index.spec.js:
--------------------------------------------------------------------------------
1 | import * as actions from './index'
2 |
3 | describe('todo actions', () => {
4 | it('addTodo should create ADD_TODO action', () => {
5 | expect(actions.addTodo('Use Redux')).toEqual({
6 | type: 'ADD_TODO',
7 | id: 0,
8 | text: 'Use Redux'
9 | })
10 | })
11 |
12 | it('setVisibilityFilter should create SET_VISIBILITY_FILTER action', () => {
13 | expect(actions.setVisibilityFilter('active')).toEqual({
14 | type: 'SET_VISIBILITY_FILTER',
15 | filter: 'active'
16 | })
17 | })
18 |
19 | it('toggleTodo should create TOGGLE_TODO action', () => {
20 | expect(actions.toggleTodo(1)).toEqual({
21 | type: 'TOGGLE_TODO',
22 | id: 1
23 | })
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/reducers/todos.js:
--------------------------------------------------------------------------------
1 | const todo = (state, action) => {
2 | switch (action.type) {
3 | case 'ADD_TODO':
4 | return {
5 | id: action.id,
6 | text: action.text,
7 | completed: false
8 | }
9 | case 'TOGGLE_TODO':
10 | if (state.id !== action.id) {
11 | return state
12 | }
13 |
14 | return {
15 | ...state,
16 | completed: !state.completed
17 | }
18 | default:
19 | return state
20 | }
21 | }
22 |
23 | const todos = (state = [], action) => {
24 | switch (action.type) {
25 | case 'ADD_TODO':
26 | return [
27 | ...state,
28 | todo(undefined, action)
29 | ]
30 | case 'TOGGLE_TODO':
31 | return state.map(t =>
32 | todo(t, action)
33 | )
34 | default:
35 | return state
36 | }
37 | }
38 |
39 | export default todos
40 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/containers/FilterLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import recycle from 'recycle'
3 | import { setVisibilityFilter } from '../actions'
4 |
5 | const FilterLink = recycle({
6 | dispatch (sources) {
7 | return [
8 | sources.select('a')
9 | .addListener('onClick')
10 | .map(e => e.preventDefault())
11 | .withLatestFrom(sources.props)
12 | .map(([e, props]) => setVisibilityFilter(props.filter))
13 | ]
14 | },
15 |
16 | update (sources) {
17 | return [
18 | sources.store
19 | // maping store to the component state
20 | .reducer((state, store) => store)
21 | ]
22 | },
23 |
24 | view (props, state) {
25 | if (props.filter === state.visibilityFilter) {
26 | return {props.children}
27 | }
28 |
29 | return {props.children}
30 | }
31 | })
32 |
33 | export default FilterLink
34 |
--------------------------------------------------------------------------------
/src/getNodeSelectors.js:
--------------------------------------------------------------------------------
1 | function getNodeSelectors (nodeName, attrs) {
2 | let selectors = []
3 |
4 | let tag = (typeof nodeName === 'string') ? nodeName : undefined
5 | let id = (attrs) ? attrs.id : undefined
6 | let className = (attrs) ? attrs.className : undefined
7 | let functionSelector = (typeof nodeName === 'function' || typeof nodeName === 'object') ? nodeName : undefined
8 |
9 | if (tag) {
10 | selectors.push({ selector: tag, selectorType: 'tag' })
11 | }
12 |
13 | if (functionSelector) {
14 | selectors.push({ selector: functionSelector, selectorType: 'tag' })
15 | }
16 |
17 | if (className) {
18 | let classes = className.split(' ').map(className => ({ selector: className, selectorType: 'class' }))
19 | selectors = selectors.concat(classes)
20 | }
21 |
22 | if (id) {
23 | selectors.push({ selector: id, selectorType: 'id' })
24 | }
25 |
26 | return selectors
27 | }
28 |
29 | export default getNodeSelectors
30 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/containers/VisibleTodoList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import recycle from 'recycle'
3 | import { toggleTodo } from '../actions'
4 | import TodoList from '../components/TodoList'
5 |
6 | const getVisibleTodos = (todos, filter) => {
7 | switch (filter) {
8 | case 'SHOW_ALL':
9 | return todos
10 | case 'SHOW_COMPLETED':
11 | return todos.filter(t => t.completed)
12 | case 'SHOW_ACTIVE':
13 | return todos.filter(t => !t.completed)
14 | default:
15 | throw new Error('Unknown filter: ' + filter)
16 | }
17 | }
18 |
19 | const VisibleTodoList = recycle({
20 | dispatch (sources) {
21 | return sources.select(TodoList)
22 | .addListener('onTodoClick')
23 | .map(toggleTodo)
24 | },
25 |
26 | update (sources) {
27 | return sources.store
28 | // maping store to the component state
29 | .reducer((state, store) => store)
30 | },
31 |
32 | view (props, state) {
33 | return
34 | }
35 | })
36 |
37 | export default VisibleTodoList
38 |
--------------------------------------------------------------------------------
/tests/index.js:
--------------------------------------------------------------------------------
1 | // functional tests
2 | /* global expect, it */
3 | import recycle from '../src'
4 | import React from 'react'
5 | import {shallow} from 'enzyme'
6 |
7 | it('should change label', () => {
8 | const CheckboxWithLabel = recycle({
9 | initialState: { isChecked: false },
10 |
11 | update (sources) {
12 | return [
13 | sources.select('input')
14 | .addListener('onChange')
15 | .reducer(function (state) {
16 | state.isChecked = !state.isChecked
17 | return state
18 | })
19 | ]
20 | },
21 |
22 | view (props, state) {
23 | return (
24 |
25 |
29 | {state.isChecked ? props.labelOn : props.labelOff}
30 |
31 | )
32 | }
33 | })
34 |
35 | const checkbox = shallow(
36 |
37 | )
38 |
39 | expect(checkbox.text()).toEqual('Off')
40 | checkbox.find('input').simulate('change')
41 | expect(checkbox.text()).toEqual('On')
42 | })
43 |
44 |
--------------------------------------------------------------------------------
/examples/simple/src/Timer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import recycle from 'recycle'
3 | import { reducer } from 'recycle/lib/customRxOperators' // optional
4 | import Rx from 'rxjs'
5 |
6 | const Timer = recycle({
7 | initialState: {
8 | secondsElapsed: 0,
9 | counter: 0
10 | },
11 |
12 | update (sources) {
13 | return [
14 | sources.select('button')
15 | .addListener('onClick')
16 | .reducer(function (state) {
17 | return {
18 | ...state,
19 | counter: state.counter + 1
20 | }
21 | }),
22 |
23 | Rx.Observable.interval(1000)
24 | // if you don't want to use custom Rx operator
25 | // you can use "let"
26 | .let(reducer(function (state) {
27 | return {
28 | ...state,
29 | secondsElapsed: state.secondsElapsed + 1
30 | }
31 | }))
32 | ]
33 | },
34 |
35 | view (props, state) {
36 | return (
37 |
38 |
Seconds Elapsed: {state.secondsElapsed}
39 |
Times Clicked: {state.counter}
40 |
Click Me
41 |
42 | )
43 | }
44 | })
45 |
46 | export default Timer
47 |
--------------------------------------------------------------------------------
/examples/simple/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 | React App
17 |
18 |
19 |
20 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/containers/AddTodo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import recycle from 'recycle'
3 | import { addTodo } from '../actions'
4 |
5 | const AddTodo = recycle({
6 | initialState: {
7 | inputVal: ''
8 | },
9 |
10 | dispatch (sources) {
11 | return [
12 | sources.select('form')
13 | .addListener('onSubmit')
14 | .withLatestFrom(sources.state)
15 | .map(([e, state]) => addTodo(state.inputVal))
16 | ]
17 | },
18 |
19 | update (sources) {
20 | return [
21 | sources.select('input')
22 | .addListener('onChange')
23 | .reducer(function (state, e) {
24 | return {
25 | ...state,
26 | inputVal: e.target.value
27 | }
28 | }),
29 |
30 | sources.select('form')
31 | .addListener('onSubmit')
32 | .reducer(function (state, e) {
33 | e.preventDefault()
34 | return {
35 | ...state,
36 | inputVal: ''
37 | }
38 | })
39 | ]
40 | },
41 |
42 | view (props, state) {
43 | return (
44 |
45 |
51 |
52 | )
53 | }
54 | })
55 |
56 | export default AddTodo
57 |
--------------------------------------------------------------------------------
/src/customCreateElement.js:
--------------------------------------------------------------------------------
1 | import getNodeSelectors from './getNodeSelectors'
2 |
3 | const customCreateElement = Rx => (listeners, nodeStreams, originalCreateElement) => function () {
4 | const possibleSelectors = getNodeSelectors(arguments['0'], arguments['1'])
5 |
6 | possibleSelectors.forEach(({ selectorType, selector }) => {
7 | listeners
8 | .filter(ref => ref.selector === selector)
9 | .filter(ref => ref.selectorType === selectorType)
10 | .forEach(registredRef => {
11 | let ref = {
12 | selector,
13 | selectorType,
14 | event: registredRef.event
15 | }
16 | if (!arguments['1']) {
17 | arguments['1'] = {}
18 | }
19 | if (typeof arguments['1'][ref.event] === 'function') {
20 | ref.stream = new Rx.Subject()
21 | let customFunction = arguments['1'][ref.event]
22 | arguments['1'][ref.event] = function () {
23 | let event = customFunction.apply(this, arguments)
24 | ref.stream.next(event)
25 | }
26 | } else {
27 | ref.stream = new Rx.Subject()
28 | arguments['1'][ref.event] = function () {
29 | let event = arguments['0']
30 | ref.stream.next(event)
31 | }
32 | }
33 | nodeStreams.push(ref)
34 | })
35 | })
36 |
37 | return originalCreateElement.apply(this, arguments)
38 | }
39 |
40 | export default customCreateElement
41 |
--------------------------------------------------------------------------------
/examples/redux-todos/README.md:
--------------------------------------------------------------------------------
1 | # Redux Todos Example
2 |
3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed.
4 |
5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information.
6 |
7 | ## Available Scripts
8 |
9 | In the project directory, you can run:
10 |
11 | ### `npm start`
12 |
13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
15 |
16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console.
18 |
19 | ### `npm run build`
20 |
21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance.
23 |
24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed!
26 |
27 | ### `npm run eject`
28 |
29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
30 |
31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
32 |
33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
34 |
35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
36 |
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "recycle",
3 | "version": "3.0.0",
4 | "description": "A functional and reactive library for React",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "clean": "rm -rf dist && mkdir dist && rm -rf lib && mkdir lib",
8 | "test": "jest",
9 | "test:watch": "jest --watch",
10 | "test:coverage": "jest --coverage",
11 | "prepublish": "npm run build && npm run build:umd && npm run build:umd:min",
12 | "build:umd": "browserify lib/index.js -o dist/recycle.js -s recycle && echo \"recycle = recycle.default;\" >> dist/recycle.js",
13 | "build:umd:min": "NODE_ENV=production uglifyjs --compress --mangle -o dist/recycle.min.js -- dist/recycle.js",
14 | "build": "npm run clean && babel src -d ./lib/"
15 | },
16 | "jest": {
17 | "testMatch": [
18 | "**/tests/**/*.js?(x)",
19 | "**/src/?(*.)(spec|test).js?(x)"
20 | ]
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/recyclejs/recycle.git"
25 | },
26 | "author": "Domagoj Kriskovic",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/recyclejs/recycle/issues"
30 | },
31 | "homepage": "https://recycle.js.org",
32 | "devDependencies": {
33 | "@types/prop-types": "^15.5.2",
34 | "@types/react": "^16.0.12",
35 | "babel-cli": "^6.18.0",
36 | "babel-plugin-add-module-exports": "^0.2.1",
37 | "babel-plugin-syntax-jsx": "^6.18.0",
38 | "babel-plugin-transform-object-rest-spread": "^6.16.0",
39 | "babel-plugin-transform-react-jsx": "^6.24.1",
40 | "babel-preset-es2015": "^6.18.0",
41 | "babel-register": "^6.18.0",
42 | "babelify": "^7.3.0",
43 | "browserify": "^13.3.0",
44 | "classnames": "^2.2.5",
45 | "css-loader": "^0.25.0",
46 | "enzyme": "^2.8.2",
47 | "eslint": "^3.9.1",
48 | "eslint-config-standard": "^6.2.1",
49 | "eslint-config-standard-react": "^4.2.0",
50 | "eslint-plugin-promise": "^3.3.2",
51 | "eslint-plugin-react": "^6.7.1",
52 | "eslint-plugin-standard": "^2.0.1",
53 | "gitbook-cli": "^2.3.0",
54 | "jest": "^20.0.1",
55 | "prop-types": "^15.0.0",
56 | "react": "^15.0.0",
57 | "react-dom": "^15.5.4",
58 | "react-router": "^3.0.0",
59 | "react-test-renderer": "^15.5.4",
60 | "redux": "^3.6.0",
61 | "rxjs": "^5.0.0",
62 | "style-loader": "^0.13.1",
63 | "uglify-js": "^2.7.5"
64 | },
65 | "peerDependencies": {
66 | "prop-types": "15.x || 16.x",
67 | "react": "15.x || 16.x",
68 | "rxjs": "5.x"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/examples/redux-todos/src/reducers/todos.spec.js:
--------------------------------------------------------------------------------
1 | import todos from './todos'
2 |
3 | describe('todos reducer', () => {
4 | it('should handle initial state', () => {
5 | expect(
6 | todos(undefined, {})
7 | ).toEqual([])
8 | })
9 |
10 | it('should handle ADD_TODO', () => {
11 | expect(
12 | todos([], {
13 | type: 'ADD_TODO',
14 | text: 'Run the tests',
15 | id: 0
16 | })
17 | ).toEqual([
18 | {
19 | text: 'Run the tests',
20 | completed: false,
21 | id: 0
22 | }
23 | ])
24 |
25 | expect(
26 | todos([
27 | {
28 | text: 'Run the tests',
29 | completed: false,
30 | id: 0
31 | }
32 | ], {
33 | type: 'ADD_TODO',
34 | text: 'Use Redux',
35 | id: 1
36 | })
37 | ).toEqual([
38 | {
39 | text: 'Run the tests',
40 | completed: false,
41 | id: 0
42 | }, {
43 | text: 'Use Redux',
44 | completed: false,
45 | id: 1
46 | }
47 | ])
48 |
49 | expect(
50 | todos([
51 | {
52 | text: 'Run the tests',
53 | completed: false,
54 | id: 0
55 | }, {
56 | text: 'Use Redux',
57 | completed: false,
58 | id: 1
59 | }
60 | ], {
61 | type: 'ADD_TODO',
62 | text: 'Fix the tests',
63 | id: 2
64 | })
65 | ).toEqual([
66 | {
67 | text: 'Run the tests',
68 | completed: false,
69 | id: 0
70 | }, {
71 | text: 'Use Redux',
72 | completed: false,
73 | id: 1
74 | }, {
75 | text: 'Fix the tests',
76 | completed: false,
77 | id: 2
78 | }
79 | ])
80 | })
81 |
82 | it('should handle TOGGLE_TODO', () => {
83 | expect(
84 | todos([
85 | {
86 | text: 'Run the tests',
87 | completed: false,
88 | id: 1
89 | }, {
90 | text: 'Use Redux',
91 | completed: false,
92 | id: 0
93 | }
94 | ], {
95 | type: 'TOGGLE_TODO',
96 | id: 1
97 | })
98 | ).toEqual([
99 | {
100 | text: 'Run the tests',
101 | completed: true,
102 | id: 1
103 | }, {
104 | text: 'Use Redux',
105 | completed: false,
106 | id: 0
107 | }
108 | ])
109 | })
110 |
111 | })
112 |
--------------------------------------------------------------------------------
/src/component.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import forceArray from './forceArray'
3 | import shallowClone from './shallowClone'
4 | import customRxOperators from './customRxOperators'
5 | import _makeUpdateNodeStreams from './updateNodeStreams'
6 | import _makeRegisterListeners from './registerListeners'
7 | import _makeCustomCreateElement from './customCreateElement'
8 |
9 | export default (React, Rx) => function recycle (component) {
10 | const customCreateElement = _makeCustomCreateElement(Rx)
11 | const registerListeners = _makeRegisterListeners(Rx)
12 | const updateNodeStreams = _makeUpdateNodeStreams(Rx)
13 | const originalCreateElement = React.createElement
14 | customRxOperators(Rx)
15 |
16 | class RecycleComponent extends React.Component {
17 | componentWillMount () {
18 | this.listeners = []
19 | this.nodeStreams = []
20 |
21 | this.sources = {
22 | select: registerListeners(this.listeners, 'tag'),
23 | selectClass: registerListeners(this.listeners, 'class'),
24 | selectId: registerListeners(this.listeners, 'id'),
25 | lifecycle: new Rx.Subject(),
26 | state: new Rx.Subject(),
27 | props: new Rx.Subject()
28 | }
29 |
30 | this.componentState = {...component.initialState}
31 |
32 | // create redux store stream
33 | if (this.context && this.context.store) {
34 | const store = this.context.store
35 | this.sources.store = new Rx.BehaviorSubject(store.getState())
36 | store.subscribe(() => {
37 | this.sources.store.next(store.getState())
38 | })
39 | }
40 |
41 | // dispatch events to redux store
42 | if (component.dispatch && this.context && this.context.store) {
43 | const events$ = Rx.Observable.merge(...forceArray(component.dispatch(this.sources)))
44 | this.__eventsSubsription = events$.subscribe((a) => {
45 | this.context.store.dispatch(a)
46 | })
47 | }
48 |
49 | // handling component state with update() stream
50 | this.setState(this.componentState)
51 | if (component.update) {
52 | const state$ = Rx.Observable.merge(...forceArray(component.update(this.sources)))
53 | this.__stateSubsription = state$.subscribe(newVal => {
54 | if (this.__componentMounted) {
55 | this.componentState = shallowClone(newVal.reducer(this.componentState, newVal.event))
56 | } else {
57 | this.componentState = newVal.reducer(this.componentState, newVal.event)
58 | }
59 | this.setState(this.componentState)
60 | })
61 | }
62 |
63 | if (component.effects) {
64 | const effects$ = Rx.Observable.merge(...forceArray(component.effects(this.sources)))
65 | this.__effectsSubsription = effects$.subscribe(function () {
66 | // intentionally empty
67 | })
68 | }
69 | }
70 |
71 | componentDidMount () {
72 | this.__componentMounted = true
73 | this.sources.lifecycle.next('componentDidMount')
74 | }
75 |
76 | componentDidUpdate () {
77 | updateNodeStreams(this.listeners, this.nodeStreams)
78 | this.sources.state.next(this.componentState)
79 | this.sources.props.next(this.props)
80 | this.sources.lifecycle.next('componentDidUpdate')
81 | }
82 |
83 | componentWillUnmount () {
84 | this.sources.lifecycle.next('componentWillUnmount')
85 | if (this.__stateSubsription) {
86 | this.__stateSubsription.unsubscribe()
87 | }
88 | if (this.__eventsSubsription) {
89 | this.__eventsSubsription.unsubscribe()
90 | }
91 | if (this.__effectsSubsription) {
92 | this.__effectsSubsription.unsubscribe()
93 | }
94 | }
95 |
96 | render () {
97 | this.nodeStreams = []
98 | React.createElement = customCreateElement(this.listeners, this.nodeStreams, originalCreateElement)
99 | const view = component.view(this.props, this.componentState)
100 | React.createElement = originalCreateElement
101 |
102 | updateNodeStreams(this.listeners, this.nodeStreams)
103 | this.sources.state.next(this.componentState)
104 | this.sources.props.next(this.props)
105 |
106 | return view
107 | }
108 | }
109 |
110 | RecycleComponent.contextTypes = {
111 | store: PropTypes.object
112 | }
113 | RecycleComponent.propTypes = component.propTypes
114 | RecycleComponent.displayName = component.displayName
115 |
116 | return RecycleComponent
117 | }
118 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://gitter.im/recyclejs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
2 | [](https://www.npmjs.com/package/recycle)
3 | [](https://www.npmjs.com/package/recycle)
4 |
5 | # DEPRECATED
6 | Please note that this library hasn't been updated for more than two years. It's very rarely used and I consider it deprecated.
7 |
8 | # Recycle
9 | Convert functional/reactive object description into React component.
10 |
11 | You don't need another UI framework if you want to use [RxJS](https://github.com/ReactiveX/rxjs).
12 |
13 | ## Installation
14 | ```bash
15 | npm install --save recycle
16 | ```
17 |
18 | ## Example
19 | [**Webpackbin example**](https://www.webpackbin.com/bins/-KiHSPOMjmY9tz4qYnbv)
20 |
21 | ```javascript
22 | const Timer = recycle({
23 | initialState: {
24 | secondsElapsed: 0,
25 | counter: 0
26 | },
27 |
28 | update (sources) {
29 | return [
30 | sources.select('button')
31 | .addListener('onClick')
32 | .reducer(state => {
33 | ...state,
34 | counter: state.counter + 1
35 | }),
36 |
37 | Rx.Observable.interval(1000)
38 | .reducer(state => {
39 | ...state,
40 | secondsElapsed: state.secondsElapsed + 1
41 | })
42 | ]
43 | },
44 |
45 | view (props, state) {
46 | return (
47 |
48 |
Seconds Elapsed: {state.secondsElapsed}
49 |
Times Clicked: {state.counter}
50 |
Click Me
51 |
52 | )
53 | }
54 | })
55 | ```
56 |
57 | You can also listen on child component events and define custom event handlers.
58 | Just make sure you specify what should be returned:
59 |
60 | ```javascript
61 | import CustomButton from './CustomButton'
62 |
63 | const Timer = recycle({
64 | initialState: {
65 | counter: 0
66 | },
67 |
68 | update (sources) {
69 | return [
70 | sources.select(CustomButton)
71 | .addListener('customOnClick')
72 | .reducer((state, returnedValue) => {
73 | counter: state.counter + returnedValue
74 | })
75 | ]
76 | },
77 |
78 | view (props, state) {
79 | return (
80 |
81 |
Times Clicked: {state.counter}
82 |
e.something}>Click Me
83 |
84 | )
85 | }
86 | })
87 | ```
88 |
89 | ## Replacing Redux Connect
90 | If you are using Redux,
91 | Recycle component can also be used as a container (an alternative to Redux `connect`).
92 |
93 | The advantage of this approach is that you have full control over component rerendering (components will not be "forceUpdated" magically).
94 |
95 | Also, you can listen to a specific part of the state and update your component only if that property is changed.
96 |
97 | ```javascript
98 | export default recycle({
99 | dispatch (sources) {
100 | return [
101 | sources.select('div')
102 | .addListener('onClick')
103 | .mapTo({ type: 'REDUX_ACTION_TYPE', text: 'hello from recycle' })
104 | ]
105 | },
106 |
107 | update (sources) {
108 | return [
109 | sources.store
110 | .reducer(function (state, store) {
111 | return store
112 | })
113 |
114 | /**
115 | * Example of a subscription on a specific store property
116 | * with distinctUntilChanged() component will be updated only when that property is changed
117 | *
118 | * sources.store
119 | * .map(s => s.specificProperty)
120 | * .distinctUntilChanged()
121 | * .reducer(function (state, specificProperty) {
122 | * state.something = specificProperty
123 | * return state
124 | * })
125 | */
126 | ]
127 | },
128 |
129 | view (props, state) {
130 | return Number of todos: {store.todos.length}
131 | }
132 | })
133 | ```
134 |
135 | ## Effects
136 | If you don't need to update a component local state or dispatch Redux action,
137 | but you still need to react to some kind of async operation, you can use `effects`.
138 |
139 | Recycle will subscribe to this stream but it will not use it.
140 | It is intended for making side effects (like calling callback functions passed from a parent component)
141 |
142 | ```javascript
143 | const Timer = recycle({
144 |
145 | effects (sources) {
146 | return [
147 | sources.select('input')
148 | .addListener('onKeyPress')
149 | .withLatestFrom(sources.props)
150 | .map(([e, props]) => {
151 | props.callParentFunction(e.target.value)
152 | })
153 | ]
154 | },
155 |
156 | view (props) {
157 | return (
158 |
159 | )
160 | }
161 | })
162 | ```
163 |
164 | ## API
165 | Component description object accepts following properties:
166 |
167 | ```javascript
168 | {
169 | propTypes: { name: PropTypes.string },
170 | displayName: 'ComponentName',
171 | initialState: {},
172 | dispatch: function(sources) { return Observable },
173 | update: function(sources) { return Observable },
174 | effects: function(sources) { return Observable },
175 | view: function(props, state) { return JSX }
176 | }
177 | ```
178 |
179 | In `update`, `dispatch` and `effects` functions, you can use the following sources:
180 |
181 | ```javascript
182 | /**
183 | * sources.select
184 | *
185 | * select node by tag name or child component
186 | */
187 | sources.select('tag')
188 | .addListener('event')
189 |
190 | sources.select(ChildComponent)
191 | .addListener('event')
192 |
193 | /**
194 | * sources.selectClass
195 | *
196 | * select node by class name
197 | */
198 | sources.selectClass('classname')
199 | .addListener('event')
200 |
201 | /**
202 | * sources.selectId
203 | *
204 | * select node by its id
205 | */
206 | sources.selectId('node-id')
207 | .addListener('event')
208 |
209 | /**
210 | * sources.store
211 | *
212 | * If you are using redux (component is inside Provider)
213 | * sources.store will emit its state changes
214 | */
215 | sources.store
216 | .reducer(...)
217 |
218 | /**
219 | * sources.state
220 | *
221 | * Stream of current local component state
222 | */
223 | sources.select('input')
224 | .addListener('onKeyPress')
225 | .filter(e => e.key === 'Enter')
226 | .withLatestFrom(sources.state)
227 | .map(([e, state]) => state.someStateValue)
228 | .map(someStateValue => using(someStateValue))
229 |
230 | /**
231 | * sources.props
232 | *
233 | * Stream of current local component props
234 | */
235 | sources.select('input')
236 | .addListener('onKeyPress')
237 | .filter(e => e.key === 'Enter')
238 | .withLatestFrom(sources.props)
239 | .map(([e, props]) => props.somePropsValue)
240 | .map(somePropsValue => using(somePropsValue))
241 |
242 | /**
243 | * sources.lifecycle
244 | *
245 | * Stream of component lifecycle events
246 | */
247 | sources.lifecycle
248 | .filter(e => e === 'componentDidMount')
249 | .do(something)
250 | ```
251 |
252 | ## FAQ
253 |
254 | ### Why would I use it?
255 | - Greater separation of concerns between component presentation and component logic
256 | - You don't need classes so each part of a component can be defined and tested separately.
257 | - Component description is more consistent.
258 | There is no custom `handleClick` events or `this.setState` statements that you need to worry about.
259 | - The State is calculated the same way as for redux store: `state = reducer(state, action)`.
260 | - Redux container looks like a normal component and it's more clear what it does.
261 | - Easy to use in an existing React application (choose components which you wish to convert).
262 |
263 | ### Why would I NOT use it?
264 | - Observables are not your thing.
265 | - You need more control over component lifecycle (like `shouldComponentUpdate`)
266 |
267 | ### What is this? jQuery?
268 | No.
269 |
270 | Although it resembles [query selectors](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector), Recycle uses React’s inline event handlers and doesn’t rely on the DOM. Since selection is isolated per component, no child nodes can ever be accessed.
271 |
272 | ### Can I use CSS selectors?
273 | No.
274 |
275 | Since Recycle doesn't query over your nodes, selectors like `div .class` will not work.
276 |
277 | ### How does it then find selected nodes?
278 | It works by monkeypatching `React.createElement`.
279 | Before a component is rendered, for each element,
280 | if a select query is matched, recycle sets inline event listener.
281 |
282 | Each time event handler dispatches an event,
283 | it calls `selectedNode.rxSubject.next(e)`
284 |
285 | ### Can I use it with React Native?
286 | Yes.
287 |
288 | Recycle creates classical React component which can be safely used in React Native.
289 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs'
2 | import React from 'react'
3 | import * as PropTypes from 'prop-types'
4 | import { AnyAction } from 'redux'
5 |
6 |
7 | export declare const recycle: recycle.Recycle
8 | export default recycle
9 | export as namespace recycle
10 |
11 |
12 | type ReducerFn = (state: S, value?: any, store?: R) => S
13 | type ReducerObservableFn = (stream: Observable) => Observable<{ reducer: ReducerFn, event: Event }>
14 |
15 |
16 | type DomEvent =
17 | 'onClick' |
18 | 'onKeyUp' |
19 | 'onKeyDown' |
20 | 'onKeyPress' |
21 | 'onBlur' |
22 | 'onChange' |
23 | 'onClose' |
24 | 'onFocus' |
25 | 'onInput' |
26 | 'onMouseMove' |
27 | 'onMouseDown' |
28 | 'onMouseUp' |
29 | 'onMouseOut' |
30 | 'onMouseOver' |
31 | 'onScroll' |
32 | 'onSubmit' |
33 | 'onSelect' |
34 | 'onTouchMove' |
35 | 'onTouchCancel' |
36 | 'onTouchStart' |
37 | 'onWheel'
38 |
39 |
40 | declare module 'rxjs/Observable' {
41 | interface Observable {
42 | /**
43 | * Acts like a Redux reducer, it receives you local component state (and your Redux state, if you are using Redux),
44 | * and returns a new local state
45 | *
46 | * @template S Your Component local state
47 | * @template R Your Redux state
48 | * @param {(state: S, store: R) => S} reducerFn PS: Param store only appears if you are using Redux (Component is inside Provider)
49 | * @example
50 | *
51 | * sources.select('button')
52 | * .addListener('onClick')
53 | * .reducer((state, eventValue) => {
54 | * ...state,
55 | * counter: state.counter + eventValue
56 | * })
57 | *
58 | */
59 | reducer(reducerFn: ReducerFn): ReducerObservableFn
60 | }
61 | }
62 |
63 |
64 | declare namespace recycle {
65 |
66 | interface Listeners {
67 |
68 | /**
69 | * Register a new DOM listener and returns a stream of DOM events
70 | *
71 | * @param event DOM Event to listen to
72 | * @returns {Observable}
73 | * @example
74 | *
75 | * sources.select('button')
76 | * .addListener('onClick')
77 | * .reducer(state => {
78 | * ...state,
79 | * counter: state.counter + 1
80 | * }),
81 | *
82 | */
83 | addListener: (event: T) => Observable
84 | }
85 |
86 | type ReactLifeCycle =
87 | 'componentWillMount' |
88 | 'componentDidMount' |
89 | 'componentWillReceiveProps' |
90 | 'shouldComponentUpdate' |
91 | 'componentWillUpdate' |
92 | 'componentDidUpdate' |
93 | 'componentWillUnmount'
94 |
95 | interface Sources {
96 |
97 | /**
98 | * This method selects a element by tagName or a ChildComponent and returns a recycle.Listeners
99 | *
100 | * @param param
101 | * @returns {recycle.Listeners} Returns { addListener: (event: DomEvent) => Observable }
102 | * @example
103 | *
104 | * PS: Assuming i have a
105 | * sources.select('button')
106 | *
107 | * or
108 | *
109 | * PS: Assuming i have a as a child element.
110 | * sources.select(ChildComponent)
111 | *
112 | */
113 | select: (param: string | React.Component | React.StatelessComponent) => recycle.Listeners;
114 |
115 | /**
116 | * This method selects a element by a CSS class and returns a recycle.Listeners
117 | *
118 | * @param className
119 | * @returns {recycle.Listeners} Returns { addListener: (event: DomEvent) => Observable }
120 | * @example
121 | *
122 | * * PS: Assuming i have a as a child element.
123 | * sources.select('cssClass')
124 | *
125 | */
126 | selectClass: (className: string) => recycle.Listeners;
127 |
128 | /**
129 | * This method selects a element by id and returns a recycle.Listeners
130 | *
131 | * @param id
132 | * @returns {recycle.Listeners} Returns { addListener: (event: DomEvent) => Observable }
133 | * @example
134 | *
135 | * PS: Assuming i have a as a child element.
136 | * sources.select('elementId')
137 | *
138 | */
139 | selectId: (id: string) => recycle.Listeners
140 |
141 | /**
142 | * If you are using Redux (component is inside Provider) this will return a stream of your redux state
143 | *
144 | * @template R is a interface representing your Redux state
145 | * @returns {Observable} Returns a Observable with your latest Redux state
146 | * @example
147 | *
148 | * PS: Assuming R is { count: number }
149 | * sources.store.map((state: { count: number }) => state.count)
150 | *
151 | */
152 | store?: Observable
153 |
154 | /**
155 | * Returns a stream of your local component state
156 | *
157 | * @template S is a interface representing your Component state
158 | * @returns {Observable} Returns a Observable with your latest component state
159 | * @example
160 | *
161 | * sources.select('input')
162 | * .addListener('onKeyPress')
163 | * .filter(e => e.key === 'Enter')
164 | * .withLatestFrom(sources.state)
165 | * .map(([e, state]) => state.someStateValue)
166 | *
167 | */
168 | state: Observable
169 |
170 | /**
171 | * Returns a stream of component lifecycle events
172 | *
173 | * @returns {Observable}
174 | * @example
175 | *
176 | * sources.lifecycle
177 | * .filter(e => e === 'componentDidMount')
178 | * .do(something)
179 | *
180 | */
181 | lifecycle: Observable
182 | }
183 |
184 | interface Params {
185 | /**
186 | * React props passed by JSX
187 | */
188 | propTypes?: PropTypes.ValidationMap
189 |
190 | /**
191 | * Determines the html tag name
192 | */
193 | displayName?: string
194 |
195 | /**
196 | * Component's local initial state
197 | */
198 | initialState?: S
199 |
200 | /**
201 | *
202 | *
203 | * @param sources
204 | * @returns {Observable[]} Array of Redux action streams
205 | */
206 | dispatch?: (sources: recycle.Sources) => Observable[]
207 |
208 | /**
209 | * Acts like a Redux reducer for the component local state
210 | *
211 | * @param sources
212 | * @template S Interface representing the component local state
213 | * @template R Interface representing the Redux state (If using Redux)
214 | * @returns {Observable>[]}
215 | * @example
216 | *
217 | * update: (sources) => {
218 | * return [
219 | * sources.store
220 | * .reducer(function (state, store) {
221 | * return state
222 | * })
223 | * ]
224 | * },
225 | *
226 | */
227 | update?: (sources: recycle.Sources) => ReducerObservableFn[]
228 |
229 | /**
230 | * If you don't need to update a component local state or dispatch Redux action, but you still need to react
231 | * to some kind of async operation, you can use effects.
232 | *
233 | * @param sources
234 | * @returns Observable[]
235 | * @example
236 | *
237 | * effects: (sources) => {
238 | * return [
239 | * sources.select('input')
240 | * .addListener('onKeyPress')
241 | * .withLatestFrom(sources.props)
242 | * .map(([e, props]) => {
243 | * props.callParentFunction(e.target.value)
244 | * })
245 | * ]
246 | * }
247 | *
248 | */
249 | effects?: (sources: recycle.Sources) => Observable[]
250 |
251 | /**
252 | * Returns the JSX to be rendered for the component
253 | *
254 | * @param props
255 | * @param state
256 | * @returns {JSX.Element}
257 | * @example
258 | *
259 | * view: (props, state) =>
260 | *
261 | *
Seconds Elapsed: {state.secondsElapsed}
262 | *
Times Clicked: {state.counter}
263 | *
Click Me
264 | *
265 | *
266 | */
267 | view?: (props: P, state: S) => JSX.Element
268 | }
269 |
270 | interface Recycle {
271 | (params: recycle.Params
): React.ComponentClass
272 | }
273 | }
274 |
--------------------------------------------------------------------------------