├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── example ├── README.md ├── package.json ├── public │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── components │ ├── Fetch.js │ └── Toggle.js │ └── index.js ├── package.json ├── rollup.config.js ├── src ├── __tests__ │ └── index.js ├── index.js └── utils.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false 5 | }], 6 | "stage-0", 7 | "react" 8 | ], 9 | "plugins": [ 10 | "external-helpers" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "es6": true 5 | }, 6 | "extends": ["plugin:prettier/recommended"], 7 | "plugins": ["react"] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | 11 | # misc 12 | .DS_Store 13 | .idea 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paul Matyukov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-with 2 | 3 | ⚡️Awesome render props components creation 4 | 5 | [![NPM](https://img.shields.io/npm/v/react-with.svg)](https://www.npmjs.com/package/react-with) 6 | 7 | Features 8 | - simple api 9 | - lifecycle support 10 | 11 | ## Install 12 | ```bash 13 | yarn add react-with --dev 14 | or 15 | npm install --save react-with 16 | ``` 17 | 18 | ## Usage 19 | Consider Toggle component example 20 | ```jsx 21 | import React from 'react'; 22 | import With from 'react-with'; 23 | 24 | const onToggle = ({ state, setState }, event) => 25 | setState(prevState => ({ on: !prevState.on })); 26 | 27 | const Toggle = ({ children, initial }) => ( 28 | 29 | {({ on, toggle }) => ( 30 | 31 | )} 32 | 33 | ); 34 | 35 | ``` 36 | 37 | Props: 38 | 39 | *state* - initial state object 40 | 41 | 42 | *lifecycle* - object with React lifecycle hooks (componentDidMount, componentWillUnmount, etc) (see Fetch example) 43 | 44 | 45 | *render* - if there is a necessity to use instead of children function 46 | 47 | Each function passed to With Component receives *self* as a first argument. 48 | 49 | *self* is an object and contains current state, setState, and other passed props to With component 50 | 51 | ## Examples 52 | #### Toggle 53 | ```jsx 54 | import React from 'react'; 55 | 56 | import With from 'react-with'; 57 | 58 | const onToggle = ({ state, setState }) => 59 | setState(prevState => ({ on: !prevState.on })); 60 | 61 | const Toggle = ({ children, initial }) => ( 62 | 63 | {children} 64 | 65 | ); 66 | 67 | // use 68 | 69 | {({ on, toggle }) => ( 70 | 71 | )} 72 | 73 | 74 | 75 | ``` 76 | 77 | #### Fetch 78 | 79 | ```jsx 80 | import React from 'react'; 81 | import PropTypes from 'prop-types'; 82 | 83 | import With from 'react-with'; 84 | 85 | const componentDidMount = ({ setState, url }) => { 86 | setState({ loading: true }); 87 | 88 | fetch(url) 89 | .then(response => response.json()) 90 | .then(json => 91 | setState({ 92 | result: json, 93 | loading: false 94 | }) 95 | ); 96 | }; 97 | 98 | const Fetch = ({ url, children, render }) => ( 99 | 107 | {children} 108 | ); 109 | 110 | // use 111 | const url = 'https://jsonplaceholder.typicode.com/comments'; 112 | 113 | {({ loading, result }) => 114 | loading ? ( 115 | 'Loading...' 116 | ) : ( 117 |
{result.map(item => ( 118 |
{item.name}
) 119 | )}
120 | ) 121 | } 122 |
123 | ``` 124 | 125 | ## License 126 | 127 | MIT © [matpaul](https://github.com/matpaul) 128 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 2 | 3 | Below you will find some information on how to perform common tasks.
4 | You can find the most recent version of this guide [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md). 5 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-with-example", 3 | "homepage": "https://matpaul.github.io/react-with", 4 | "version": "0.0.0", 5 | "private": true, 6 | "license": "MIT", 7 | "dependencies": { 8 | "prop-types": "^15.6.1", 9 | "react": "^16.2.0", 10 | "react-dom": "^16.2.0", 11 | "react-with": "link:..", 12 | "react-scripts": "^1.1.1" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | react-with 11 | 12 | 13 | 14 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-with", 3 | "name": "react-with", 4 | "start_url": "./index.html", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Toggle from './components/Toggle'; 4 | import Fetch from './components/Fetch'; 5 | 6 | const url = 'https://jsonplaceholder.typicode.com/comments'; 7 | 8 | const App = () => ( 9 |
10 |

Toggle Example

11 | 12 | {({ on, toggle }) => ( 13 | 14 | )} 15 | 16 | 17 |

Fetch example

18 | 19 | {({ loading, result }) => 20 | loading ? ( 21 | 'Loading...' 22 | ) : ( 23 |
{result.map(item =>
{item.name}
)}
24 | ) 25 | } 26 |
27 |
28 | ); 29 | export default App; 30 | -------------------------------------------------------------------------------- /example/src/components/Fetch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import With from 'react-with'; 5 | 6 | const componentDidMount = ({ setState, url }) => { 7 | setState({ loading: true }); 8 | 9 | if (!url) { 10 | return; 11 | } 12 | 13 | fetch(url) 14 | .then(response => response.json()) 15 | .then(json => 16 | setState({ 17 | result: json, 18 | loading: false 19 | }) 20 | ); 21 | }; 22 | 23 | const Fetch = ({ url, children, render }) => ( 24 | 32 | {children} 33 | 34 | ); 35 | 36 | Fetch.propTypes = { 37 | url: PropTypes.string.isRequired, 38 | children: PropTypes.func 39 | }; 40 | 41 | export default Fetch; 42 | -------------------------------------------------------------------------------- /example/src/components/Toggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import With from 'react-with'; 5 | 6 | const onToggle = ({ state, setState }) => 7 | setState(state => ({ on: !state.on })); 8 | 9 | const Toggle = ({ children, initial }) => ( 10 | 11 | {children} 12 | 13 | ); 14 | 15 | Toggle.defaultProps = { 16 | initial: false 17 | }; 18 | 19 | Toggle.propTypes = { 20 | children: PropTypes.func, 21 | initial: PropTypes.bool 22 | }; 23 | 24 | export default Toggle; 25 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-with", 3 | "version": "0.2.0", 4 | "description": "⚡️Awesome render props components creation", 5 | "author": "matpaul", 6 | "license": "MIT", 7 | "repository": "matpaul/react-with", 8 | "main": "dist/index.js", 9 | "module": "dist/index.es.js", 10 | "jsnext:main": "dist/index.es.js", 11 | "scripts": { 12 | "precommit": "lint-staged", 13 | "test": "cross-env CI=1 react-scripts test --env=jsdom", 14 | "test:watch": "react-scripts test --env=jsdom", 15 | "build": "rollup -c", 16 | "start": "rollup -c -w", 17 | "prepublish": "yarn run test && yarn run build" 18 | }, 19 | "peerDependencies": { 20 | "prop-types": "^15.5.4", 21 | "react": "^15.0.0 || ^16.0.0", 22 | "react-dom": "^15.0.0 || ^16.0.0" 23 | }, 24 | "lint-staged": { 25 | "*.{js,json,css,md}": [ 26 | "prettier --write", 27 | "git add" 28 | ] 29 | }, 30 | "devDependencies": { 31 | "babel-core": "^6.26.0", 32 | "babel-eslint": "^8.2.1", 33 | "babel-plugin-external-helpers": "^6.22.0", 34 | "babel-plugin-transform-runtime": "^6.23.0", 35 | "babel-preset-env": "^1.6.0", 36 | "babel-preset-react": "^6.24.1", 37 | "babel-preset-stage-0": "^6.24.1", 38 | "cross-env": "^5.1.4", 39 | "eslint": "^4.19.1", 40 | "eslint-config-prettier": "^2.9.0", 41 | "eslint-plugin-prettier": "^2.6.0", 42 | "husky": "^0.14.3", 43 | "lint-staged": "^7.2.0", 44 | "react": "^16.2.0", 45 | "react-dom": "^16.2.0", 46 | "react-scripts": "^1.1.1", 47 | "rollup": "^0.54.0", 48 | "rollup-plugin-babel": "^3.0.3", 49 | "rollup-plugin-commonjs": "^8.2.1", 50 | "rollup-plugin-node-resolve": "^3.0.2", 51 | "rollup-plugin-peer-deps-external": "^2.0.0", 52 | "rollup-plugin-postcss": "^1.1.0", 53 | "rollup-plugin-url": "^1.3.0" 54 | }, 55 | "files": [ 56 | "dist" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import external from 'rollup-plugin-peer-deps-external'; 4 | import postcss from 'rollup-plugin-postcss'; 5 | import resolve from 'rollup-plugin-node-resolve'; 6 | import url from 'rollup-plugin-url'; 7 | 8 | import pkg from './package.json'; 9 | 10 | export default { 11 | input: 'src/index.js', 12 | output: [ 13 | { 14 | file: pkg.main, 15 | format: 'cjs' 16 | }, 17 | { 18 | file: pkg.module, 19 | format: 'es' 20 | } 21 | ], 22 | plugins: [ 23 | external(), 24 | postcss({ 25 | modules: true 26 | }), 27 | url(), 28 | babel({ 29 | exclude: 'node_modules/**', 30 | plugins: ['external-helpers'] 31 | // externalHelpers: true 32 | }), 33 | resolve(), 34 | commonjs() 35 | ] 36 | }; 37 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import With from './'; 2 | 3 | describe('With', () => { 4 | it('is truthy', () => { 5 | expect(With).toBeTruthy(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { isFn } from './utils'; 5 | 6 | const { shape, func, object } = PropTypes; 7 | 8 | class With extends React.Component { 9 | static defaultProps = { 10 | state: {}, 11 | lifecycle: {} 12 | }; 13 | 14 | static propTypes = { 15 | state: object, 16 | render: func, 17 | children: func, 18 | lifecycle: shape({ 19 | componentDidMount: func, 20 | componentWillUnmount: func, 21 | shouldComponentUpdate: func, 22 | componentDidUpdate: func, 23 | componentDidCatch: func 24 | }) 25 | }; 26 | 27 | static hooks = [ 28 | 'componentDidMount', 29 | 'componentWillUnmount', 30 | 'shouldComponentUpdate', 31 | 'componentDidUpdate', 32 | 'componentDidCatch' 33 | ]; 34 | 35 | set = (...args) => this.setState(...args); 36 | 37 | constructor(props) { 38 | super(props); 39 | 40 | const { state, lifecycle, children, ...other } = props; 41 | 42 | this.state = state; 43 | this.fns = []; 44 | 45 | Object.keys(other) 46 | .map(name => [name, other[name]]) // entries need polyfill 47 | .filter(([name, value]) => isFn(value)) 48 | .forEach(([name, fn]) => { 49 | this.fns[name] = (...args) => { 50 | return fn( 51 | { ...props, state: this.state, setState: this.set, ...this.fns }, 52 | ...args 53 | ); 54 | }; 55 | }); 56 | 57 | With.hooks.forEach(name => { 58 | const hook = lifecycle[name]; 59 | 60 | if (isFn(hook)) { 61 | this[name] = (...args) => { 62 | hook( 63 | { ...other, setState: this.set, state: this.state, ...this.fns }, 64 | args 65 | ); 66 | }; 67 | } 68 | }); 69 | } 70 | 71 | render() { 72 | const { children, render } = this.props; 73 | 74 | const args = { ...this.state, ...this.fns }; 75 | 76 | const renderTo = isFn(children) ? children : render; 77 | 78 | return renderTo(args); 79 | } 80 | } 81 | 82 | export default With; 83 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const isFn = arg => typeof arg === 'function'; 2 | --------------------------------------------------------------------------------