├── tests
├── .eslintrc
└── index-test.js
├── .gitignore
├── nwb.config.js
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── package.json
├── src
└── index.js
├── demo
└── src
│ └── index.js
├── .all-contributorsrc
└── README.md
/tests/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /demo/dist
3 | /es
4 | /lib
5 | /node_modules
6 | /umd
7 | npm-debug.log*
8 |
--------------------------------------------------------------------------------
/nwb.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | type: 'react-component',
3 | npm: {
4 | esModules: true,
5 | umd: false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: node_js
4 | node_js:
5 | - 8
6 |
7 | before_install:
8 | - npm install codecov.io coveralls
9 |
10 | after_success:
11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js
12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
13 |
14 | branches:
15 | only:
16 | - master
17 |
--------------------------------------------------------------------------------
/tests/index-test.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import {render, unmountComponentAtNode} from 'react-dom'
4 |
5 | import Component from 'src/'
6 |
7 | describe('Component', () => {
8 | let node
9 |
10 | beforeEach(() => {
11 | node = document.createElement('div')
12 | })
13 |
14 | afterEach(() => {
15 | unmountComponentAtNode(node)
16 | })
17 |
18 | it('displays a welcome message', () => {
19 | render(, node, () => {
20 | expect(node.innerHTML).toContain('Welcome to React components')
21 | })
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Prerequisites
2 |
3 | [Node.js](http://nodejs.org/) >= 6 must be installed.
4 |
5 | ## Installation
6 |
7 | - Running `npm install` in the component's root directory will install everything you need for development.
8 |
9 | ## Demo Development Server
10 |
11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading.
12 |
13 | ## Running Tests
14 |
15 | - `npm test` will run the tests once.
16 |
17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`.
18 |
19 | - `npm run test:watch` will run the tests on every change.
20 |
21 | ## Building
22 |
23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app.
24 |
25 | - `npm run clean` will delete built resources.
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Astrocoders
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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "epitath",
3 | "version": "1.0.0-beta.2",
4 | "description": "Compose HOCs imperatively like async/await. No callback hell!",
5 | "main": "lib/index.js",
6 | "module": "es/index.js",
7 | "files": [
8 | "css",
9 | "es",
10 | "lib",
11 | "umd"
12 | ],
13 | "scripts": {
14 | "contributors:add": "all-contributors add",
15 | "contributors:generate": "all-contributors generate",
16 | "build": "nwb build-react-component",
17 | "clean": "nwb clean-module && nwb clean-demo",
18 | "prepublishOnly": "npm run build",
19 | "start": "nwb serve-react-demo",
20 | "test": "nwb test-react",
21 | "test:coverage": "nwb test-react --coverage",
22 | "test:watch": "nwb test-react --server",
23 | "deploy": "gh-pages -d demo/dist"
24 | },
25 | "dependencies": {
26 | "immutagen": "^1.0.7"
27 | },
28 | "peerDependencies": {
29 | "react": "16.x"
30 | },
31 | "devDependencies": {
32 | "all-contributors-cli": "^5.4.0",
33 | "formik": "^1.3.0",
34 | "gh-pages": "^1.2.0",
35 | "nwb": "0.23.x",
36 | "react": "^16.4.2",
37 | "react-dom": "^16.4.2"
38 | },
39 | "author": "Astrocoders",
40 | "homepage": "https://github.com/Astrocoders/epitath",
41 | "license": "MIT",
42 | "repository": "",
43 | "keywords": [
44 | "react-component",
45 | "hocs",
46 | "render-props",
47 | "compose"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import immutagen from 'immutagen'
3 |
4 | const compose = ({ next, value }) => next
5 | ? React.cloneElement(value, null, values => compose(next(values)))
6 | : value
7 |
8 | export default Component => {
9 | const original = Component.prototype.render
10 | const displayName = `EpitathContainer(${Component.displayName || 'anonymous'})`
11 |
12 | if (!original) {
13 | const generator = immutagen(Component)
14 |
15 | return Object.assign(function Epitath(props) {
16 | return compose(generator(props))
17 | }, { displayName })
18 | }
19 |
20 | Component.prototype.render = function render() {
21 | // Since we are calling a new function to be called from here instead of
22 | // from a component class, we need to ensure that the render method is
23 | // invoked against `this`. We only need to do this binding and creation of
24 | // this function once, so we cache it by adding it as a property to this
25 | // new render method which avoids keeping the generator outside of this
26 | // method's scope.
27 | if (!render.generator) {
28 | render.generator = immutagen(original.bind(this))
29 | }
30 |
31 | return compose(render.generator(this.props))
32 | }
33 |
34 | return class EpitathContainer extends React.Component {
35 | static displayName = displayName
36 | render() {
37 | return
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import React, {Fragment} from 'react'
2 | import {render} from 'react-dom'
3 | import {Formik} from 'formik'
4 |
5 | import epitath from '../../src'
6 |
7 | class Query extends React.Component {
8 | state = {loading: true, data: null}
9 |
10 | componentDidMount() {
11 | setTimeout(() => {
12 | this.setState({
13 | loading: false,
14 | data: {
15 | user: {id: '000', name: 'Nikolas Tesla', email: 'nikolas@tesla.com'},
16 | },
17 | })
18 | }, 2000)
19 | }
20 |
21 | render() {
22 | return this.props.children(this.state)
23 | }
24 | }
25 |
26 | class Time extends React.Component {
27 | state = {time: new Date()}
28 |
29 | componentDidMount() {
30 | setInterval(() => {
31 | this.setState({
32 | time: new Date(),
33 | })
34 | }, 1000)
35 | }
36 |
37 | render() {
38 | return this.props.children(this.state)
39 | }
40 | }
41 |
42 | function WrapFormik({ children, ...props}){ return }
43 |
44 | const App = epitath(function*() {
45 | console.log('Rendering again!');
46 | const {loading, data} = yield
47 | const {time} = yield
48 |
49 | if (loading) return
Loading
50 |
51 | const {
52 | values,
53 | touched,
54 | errors,
55 | handleChange,
56 | handleBlur,
57 | handleSubmit,
58 | isSubmitting,
59 | } = yield (
60 | {
67 | // same as above, but feel free to move this into a class method now.
68 | let errors = {}
69 | if (!values.email) {
70 | errors.email = 'Required'
71 | } else if (
72 | !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)
73 | ) {
74 | errors.email = 'Invalid email address'
75 | }
76 | return errors
77 | }}
78 | />
79 | )
80 |
81 | return (
82 |
83 |
{`Hello, ${data.user.name}`}
84 |
The time is {time.toLocaleString()}!
85 |
86 |
107 |
108 | )
109 | })
110 |
111 | render(, document.querySelector('#demo'))
112 |
--------------------------------------------------------------------------------
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "epitath",
3 | "projectOwner": "Astrocoders",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": [
7 | "README.md"
8 | ],
9 | "imageSize": 100,
10 | "commit": false,
11 | "contributors": [
12 | {
13 | "login": "jamiebuilds",
14 | "name": "Jamie",
15 | "avatar_url": "https://avatars0.githubusercontent.com/u/952783?v=4",
16 | "profile": "https://jamie.build/",
17 | "contributions": [
18 | "ideas",
19 | "code"
20 | ]
21 | },
22 | {
23 | "login": "eliperelman",
24 | "name": "Eli Perelman",
25 | "avatar_url": "https://avatars0.githubusercontent.com/u/285899?v=4",
26 | "profile": "http://eliperelman.com",
27 | "contributions": [
28 | "ideas",
29 | "code"
30 | ]
31 | },
32 | {
33 | "login": "grsabreu",
34 | "name": "Gabriel Rubens",
35 | "avatar_url": "https://avatars0.githubusercontent.com/u/1283200?v=4",
36 | "profile": "https://medium.com/@_gabrielrubens",
37 | "contributions": [
38 | "ideas",
39 | "code"
40 | ]
41 | },
42 | {
43 | "login": "medson10",
44 | "name": "Medson Oliveira",
45 | "avatar_url": "https://avatars0.githubusercontent.com/u/17956325?v=4",
46 | "profile": "https://github.com/medson10",
47 | "contributions": [
48 | "ideas",
49 | "code"
50 | ]
51 | },
52 | {
53 | "login": "georgelima",
54 | "name": "George Lima",
55 | "avatar_url": "https://avatars0.githubusercontent.com/u/16995184?v=4",
56 | "profile": "https://github.com/georgelima",
57 | "contributions": [
58 | "ideas",
59 | "code"
60 | ]
61 | },
62 | {
63 | "login": "eliabejr",
64 | "name": "Eliabe Júnior",
65 | "avatar_url": "https://avatars0.githubusercontent.com/u/8146889?v=4",
66 | "profile": "http://eliabejr.com",
67 | "contributions": [
68 | "code",
69 | "design"
70 | ]
71 | },
72 | {
73 | "login": "guilhermedecampo",
74 | "name": "Guilherme Decampo",
75 | "avatar_url": "https://avatars3.githubusercontent.com/u/4806269?v=4",
76 | "profile": "https://astrocoders.com",
77 | "contributions": [
78 | "ideas"
79 | ]
80 | },
81 | {
82 | "login": "gtkatakura",
83 | "name": "gtkatakura",
84 | "avatar_url": "https://avatars0.githubusercontent.com/u/8618687?v=4",
85 | "profile": "https://github.com/gtkatakura",
86 | "contributions": [
87 | "ideas",
88 | "question",
89 | "example"
90 | ]
91 | },
92 | {
93 | "login": "erjanmx",
94 | "name": "Erjan Kalybek",
95 | "avatar_url": "https://avatars0.githubusercontent.com/u/4899432?v=4",
96 | "profile": "https://mssg.me/emx",
97 | "contributions": [
98 | "doc"
99 | ]
100 | },
101 | {
102 | "login": "hanford",
103 | "name": "Jack Hanford",
104 | "avatar_url": "https://avatars1.githubusercontent.com/u/2148168?v=4",
105 | "profile": "http://jackhanford.com/",
106 | "contributions": [
107 | "doc"
108 | ]
109 | },
110 | {
111 | "login": "diegohaz",
112 | "name": "Haz",
113 | "avatar_url": "https://avatars3.githubusercontent.com/u/3068563?v=4",
114 | "profile": "https://twitter.com/diegohaz",
115 | "contributions": [
116 | "doc"
117 | ]
118 | }
119 | ]
120 | }
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # epita✞h
2 | [](#contributors)
3 |
4 |
5 | In memoriam HOCs and Render Props
6 |
7 |
8 | ### [Read the article](https://medium.com/p/9f76dd911f9e)
9 |
10 | ### Also, we think you may want to take a look on [React Hooks API now](https://reactjs.org/docs/hooks-intro.html)
11 |
12 | ```js
13 | import epitath from 'epitath'
14 | ...
15 |
16 | const App = epitath(function*() {
17 | const { loading, data } = yield
18 | const { time } = yield
19 |
20 | return (
21 |
22 | {loading ? (
23 |
Loading
24 | ) : (
25 |
26 |
{`Hello, ${data.user.name}`}
27 | The time is {time.toLocaleString()}!
28 |
29 | )}
30 |
31 | )
32 | })
33 | ```
34 |
35 | [![npm package][npm-badge]][npm]
36 |
37 | Compose HOCs imperatively like async/await. No callback hell!
38 |
39 | [Live demo](http://astrocoders.com/epitath)
40 | [Source of demo](https://github.com/Astrocoders/epitath/blob/master/demo/src/index.js#L42)
41 |
42 | [npm-badge]: https://img.shields.io/npm/v/npm-package.svg?style=flat-square
43 | [npm]: https://www.npmjs.org/package/npm-package
44 |
45 | ## Install
46 |
47 | ```
48 | yarn add epitath
49 | ```
50 | or
51 | ```
52 | npm install --save epitath
53 | ```
54 |
55 | ## Why
56 | Render props are amazing for providing more functionality but once you need to stack a bunch of them you get what recalls a painful callback hell.
57 |
58 | ```jsx
59 |
60 | {({ data }) =>
61 |
62 | {({ mutate, result })=>
63 |
66 | }
67 |
68 | }
69 |
70 | ```
71 |
72 | ## How
73 |
74 | Wait, we just mentioned "callback hell". So what if we had a function that would allow us to have a kind of sugar for continuation-passing-style à la async/await?
75 |
76 | And that's exactly what epitath is, it just takes care of the callbacks for you.
77 | The whole code is this:
78 |
79 | ```js
80 | import React from 'react'
81 | import immutagen from 'immutagen'
82 |
83 | export default component => {
84 | const generator = immutagen(component)
85 |
86 | const compose = context => {
87 | const value = context.value
88 | return context.next
89 | ? React.cloneElement(value, null, values => compose(context.next(values)))
90 | : value
91 | }
92 |
93 | function Epitath(props) {
94 | return compose(generator(props))
95 | }
96 |
97 | Epitath.displayName = `EpitathContainer(${component.displayName || 'anonymous'})`
98 |
99 | return Epitath
100 | }
101 | ```
102 |
103 | **Note that** epitath will only yield the first argument of the render function. In order to consume multiple arguments, we recommend creating a wrapper component:
104 |
105 | ```js
106 | const MutationWrapper = ({ children, ...props }) =>
107 | {(mutate, result) => children({ mutate, result })}
108 |
109 | const { mutate, result } = yield
110 | ```
111 |
112 | ## How is this different from Suspense?
113 |
114 | Suspense only allows you to evalulate a promise once. It does not allow you to trigger a re-render for a state update.
115 | And with epitath you can even use Formik, Apollo optimistic, React Powerplug and Smalldots tooling and etc!
116 |
117 | ## BTW it's epitaph not "epitath"
118 |
119 | "These Astrocoders dudes simply don't know how to spell words in English!"
120 |
121 | Actually it was intended, for 2 reasons:
122 |
123 | 1. We wanted to put a cross as icon of the package
124 | 2. Epitaph is already taken in NPM
125 |
126 |
127 | ## Contributing
128 |
129 | ### Steps to get it running
130 |
131 | Install the deps
132 | ```
133 | yarn install
134 | ```
135 |
136 | Boot the demo
137 | ```
138 | yarn start
139 | ```
140 |
141 | ### Things missing that we would like a little help
142 |
143 | - [ ] Tests
144 | - [ ] TypeScript support
145 |
146 | ### Acknowledgements
147 |
148 | Thanks @jamiebuilds for the [suggestions](https://github.com/Astrocoders/epitath/issues/1) on how simplifying the API
149 |
150 | ## Contributors
151 |
152 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
153 |
154 |
155 |
156 | | [
Jamie](https://jamie.build/)
[🤔](#ideas-jamiebuilds "Ideas, Planning, & Feedback") [💻](https://github.com/Astrocoders/epitath/commits?author=jamiebuilds "Code") | [
Eli Perelman](http://eliperelman.com)
[🤔](#ideas-eliperelman "Ideas, Planning, & Feedback") [💻](https://github.com/Astrocoders/epitath/commits?author=eliperelman "Code") | [
Gabriel Rubens](https://medium.com/@_gabrielrubens)
[🤔](#ideas-grsabreu "Ideas, Planning, & Feedback") [💻](https://github.com/Astrocoders/epitath/commits?author=grsabreu "Code") | [
Medson Oliveira](https://github.com/medson10)
[🤔](#ideas-medson10 "Ideas, Planning, & Feedback") [💻](https://github.com/Astrocoders/epitath/commits?author=medson10 "Code") | [
George Lima](https://github.com/georgelima)
[🤔](#ideas-georgelima "Ideas, Planning, & Feedback") [💻](https://github.com/Astrocoders/epitath/commits?author=georgelima "Code") | [
Eliabe Júnior](http://eliabejr.com)
[💻](https://github.com/Astrocoders/epitath/commits?author=eliabejr "Code") [🎨](#design-eliabejr "Design") | [
Guilherme Decampo](https://astrocoders.com)
[🤔](#ideas-guilhermedecampo "Ideas, Planning, & Feedback") |
157 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
158 | | [
gtkatakura](https://github.com/gtkatakura)
[🤔](#ideas-gtkatakura "Ideas, Planning, & Feedback") [💬](#question-gtkatakura "Answering Questions") [💡](#example-gtkatakura "Examples") | [
Erjan Kalybek](https://mssg.me/emx)
[📖](https://github.com/Astrocoders/epitath/commits?author=erjanmx "Documentation") | [
Jack Hanford](http://jackhanford.com/)
[📖](https://github.com/Astrocoders/epitath/commits?author=hanford "Documentation") | [
Haz](https://twitter.com/diegohaz)
[📖](https://github.com/Astrocoders/epitath/commits?author=diegohaz "Documentation") |
159 |
160 |
161 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
162 |
--------------------------------------------------------------------------------