├── .eslintrc.json
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__
├── 01_basic_spec.js
└── __snapshots__
│ └── 01_basic_spec.js.snap
├── dist
└── index.js
├── examples
├── 01_minimal
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ └── index.js
├── 02_typescript
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.tsx
│ │ ├── Counter.tsx
│ │ ├── DisplayNumber.tsx
│ │ └── index.ts
└── 03_apollo
│ ├── package.json
│ ├── public
│ └── index.html
│ └── src
│ ├── App.tsx
│ ├── NewPost.tsx
│ ├── PostList.tsx
│ ├── index.ts
│ └── mock.ts
├── package-lock.json
├── package.json
├── src
├── index.d.ts
└── index.js
├── tsconfig.json
└── webpack.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "plugins": [
4 | "@typescript-eslint",
5 | "react-hooks"
6 | ],
7 | "extends": [
8 | "plugin:@typescript-eslint/recommended",
9 | "airbnb"
10 | ],
11 | "env": {
12 | "browser": true
13 | },
14 | "settings": {
15 | "import/resolver": {
16 | "node": {
17 | "extensions": [".js", ".ts", ".tsx"]
18 | }
19 | }
20 | },
21 | "rules": {
22 | "react-hooks/rules-of-hooks": "error",
23 | "react-hooks/exhaustive-deps": "error",
24 | "@typescript-eslint/explicit-function-return-type": "off",
25 | "@typescript-eslint/indent": ["error", 2],
26 | "@typescript-eslint/prefer-interface": "off",
27 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".tsx"] }],
28 | "react/prop-types": "off",
29 | "react/jsx-one-expression-per-line": "off",
30 | "import/prefer-default-export": "off",
31 | "import/no-unresolved": ["error", { "ignore": ["react-hooks-render-props"] }]
32 | },
33 | "overrides": [{
34 | "files": ["__tests__/**/*"],
35 | "env": {
36 | "jest": true
37 | },
38 | "rules": {
39 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
40 | }
41 | }]
42 | }
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 10
4 | - 12
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [Unreleased]
4 |
5 | ## [0.2.0] - 2019-02-09
6 | ### Changed
7 | - Clean the implementation
8 | - Update dependencies
9 |
10 | ## [0.1.0] - 2018-11-04
11 | ### Added
12 | - Initial release
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018-2019 Daishi Kato
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-hooks-render-props
2 |
3 | [](https://travis-ci.com/dai-shi/react-hooks-render-props)
4 | [](https://badge.fury.io/js/react-hooks-render-props)
5 | [](https://bundlephobia.com/result?p=react-hooks-render-props)
6 |
7 | Hacky custom hook to emulate render props with Hooks API
8 |
9 | ## Introduction
10 |
11 | React Hooks API is awesome.
12 | Some of the libraries may not provide hooks API,
13 | but just render props (function as a child) API.
14 |
15 | To use such libraries, this is to provide a
16 | hacky custom hook to emulate render props (function as a child).
17 |
18 | This is not for production.
19 | It's only tested against a few small examples.
20 |
21 | ## Install
22 |
23 | ```bash
24 | npm install react-hooks-render-props
25 | ```
26 |
27 | ## Usage
28 |
29 | ```javascript
30 | import React from 'react';
31 | import { useRenderProps, wrap } from 'react-hooks-render-props';
32 |
33 | const RandomNumber = ({ children }) => (
34 |
35 | {children(Math.random())}
36 |
37 | );
38 |
39 | const NumberWithRenderProps = () => (
40 |
41 | {value => {value}
}
42 |
43 | );
44 |
45 | const NumberWithCustomHook = wrap(() => {
46 | const [value] = useRenderProps(RandomNumber);
47 | return (
48 | {value}
49 | );
50 | });
51 |
52 | const App = () => (
53 |
54 |
55 |
56 |
57 | );
58 | ```
59 |
60 | ## Examples
61 |
62 | The [examples](examples) folder contains working examples.
63 | You can run one of them with
64 |
65 | ```bash
66 | PORT=8080 npm run examples:minimal
67 | ```
68 |
69 | and open in your web browser.
70 |
71 | You can also try them in codesandbox.io:
72 | [01](https://codesandbox.io/s/github/dai-shi/react-hooks-render-props/tree/master/examples/01_minimal)
73 | [02](https://codesandbox.io/s/github/dai-shi/react-hooks-render-props/tree/master/examples/02_typescript)
74 | [03](https://codesandbox.io/s/github/dai-shi/react-hooks-render-props/tree/master/examples/03_apollo)
75 |
76 | ## Limitations
77 |
78 | Due to its hacky implementation:
79 |
80 | - It renders initially without any data.
81 | - It may not detect the change of inputs to `useRenderProps`.
82 | - And maybe some more.
83 |
--------------------------------------------------------------------------------
/__tests__/01_basic_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import React, { StrictMode } from 'react';
4 | import { render, cleanup } from 'react-testing-library';
5 |
6 | import { useRenderProps, wrap } from '../src/index';
7 |
8 | describe('basic spec', () => {
9 | afterEach(cleanup);
10 |
11 | it('should have a function', () => {
12 | expect(useRenderProps).toBeDefined();
13 | expect(wrap).toBeDefined();
14 | });
15 |
16 | it('should create the same component', () => {
17 | const SetNumber = ({ children }) => (
18 |
19 | {children(123)}
20 |
21 | );
22 | let container1;
23 | let container2;
24 | {
25 | const Component = () => (
26 |
27 | {value => {value} }
28 |
29 | );
30 | const App = () => (
31 |
32 |
33 |
34 | );
35 | container1 = render( ).container;
36 | }
37 | {
38 | const Component = wrap(() => {
39 | const [value] = useRenderProps(SetNumber);
40 | return (
41 | {value}
42 | );
43 | });
44 | const App = () => (
45 |
46 |
47 |
48 | );
49 | container2 = render( ).container;
50 | }
51 | expect(container1.innerHTML).toEqual(container2.innerHTML);
52 | expect(container1).toMatchSnapshot();
53 | expect(container2).toMatchSnapshot();
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/01_basic_spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`basic spec should create the same component 1`] = `
4 |
5 |
6 |
7 | 123
8 |
9 |
10 |
11 | `;
12 |
13 | exports[`basic spec should create the same component 2`] = `
14 |
15 |
16 |
17 | 123
18 |
19 |
20 |
21 | `;
22 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.wrap = exports.useRenderProps = void 0;
7 |
8 | var _react = require("react");
9 |
10 | function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
11 |
12 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
13 |
14 | function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
15 |
16 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
17 |
18 | var sharedObject = null;
19 |
20 | var useRenderProps = function useRenderProps(WrapperComponent, wrapperProps) {
21 | var _useState = (0, _react.useState)([]),
22 | _useState2 = _slicedToArray(_useState, 2),
23 | args = _useState2[0],
24 | setArgs = _useState2[1];
25 |
26 | var updateFlag = (0, _react.useRef)(true);
27 | sharedObject = {
28 | setArgs: setArgs,
29 | updateFlag: updateFlag,
30 | WrapperComponent: WrapperComponent,
31 | wrapperProps: wrapperProps
32 | };
33 | (0, _react.useEffect)(function () {
34 | updateFlag.current = !updateFlag.current;
35 | });
36 | return args;
37 | };
38 |
39 | exports.useRenderProps = useRenderProps;
40 |
41 | var wrap = function wrap(FunctionComponent) {
42 | return function (props) {
43 | sharedObject = null;
44 | var element = FunctionComponent(props);
45 |
46 | var _ref = sharedObject || {},
47 | setArgs = _ref.setArgs,
48 | updateFlag = _ref.updateFlag,
49 | WrapperComponent = _ref.WrapperComponent,
50 | wrapperProps = _ref.wrapperProps;
51 |
52 | if (!WrapperComponent) return element;
53 | return (0, _react.createElement)(WrapperComponent, wrapperProps, function () {
54 | if (updateFlag.current) {
55 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
56 | args[_key] = arguments[_key];
57 | }
58 |
59 | setArgs(args);
60 | }
61 |
62 | return element;
63 | });
64 | };
65 | };
66 |
67 | exports.wrap = wrap;
--------------------------------------------------------------------------------
/examples/01_minimal/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-hooks-render-props-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "latest",
7 | "react-dom": "latest",
8 | "react-hooks-render-props": "latest",
9 | "react-scripts": "^2.1.1"
10 | },
11 | "scripts": {
12 | "start": "react-scripts start",
13 | "build": "react-scripts build",
14 | "test": "react-scripts test",
15 | "eject": "react-scripts eject"
16 | },
17 | "browserslist": [
18 | ">0.2%",
19 | "not dead",
20 | "not ie <= 11",
21 | "not op_mini all"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/examples/01_minimal/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | react-hooks-render-props example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/01_minimal/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import React, { StrictMode, useState, useEffect } from 'react';
4 | import ReactDOM from 'react-dom';
5 |
6 | import { useRenderProps, wrap } from 'react-hooks-render-props';
7 |
8 | const RandomNumber = ({ name, children }) => {
9 | const [count, setCount] = useState(1);
10 | useEffect(() => {
11 | setInterval(() => {
12 | setCount(c => c + 1);
13 | }, 1000);
14 | }, []);
15 | return (
16 |
17 | {children(`${Math.random()} for ${name}`)}
18 |
Count: {count}
19 | {name === 'b' &&
}
20 |
21 | );
22 | };
23 |
24 | const ShowNumber = wrap(({ name }) => {
25 | const value = useRenderProps(RandomNumber, { name });
26 | return (
27 |
28 |
Random
29 |
Name: {name}
30 |
Value: {value}
31 | {name === 'a' &&
}
32 |
33 | );
34 | });
35 |
36 | const App = () => (
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 |
45 | ReactDOM.render( , document.getElementById('app'));
46 |
--------------------------------------------------------------------------------
/examples/02_typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-hooks-render-props-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "^16.7.6",
7 | "@types/react-dom": "^16.0.9",
8 | "react": "latest",
9 | "react-dom": "latest",
10 | "react-hooks-render-props": "latest",
11 | "react-scripts": "^2.1.1",
12 | "typescript": "^3.1.6"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "browserslist": [
21 | ">0.2%",
22 | "not dead",
23 | "not ie <= 11",
24 | "not op_mini all"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/examples/02_typescript/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | react-hooks-render-props example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/02_typescript/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import DisplayNumber from './DisplayNumber';
4 |
5 | const App = () => (
6 |
7 |
8 |
Counter
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default App;
16 |
--------------------------------------------------------------------------------
/examples/02_typescript/src/Counter.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | type Props = {
4 | children: (v: number) => React.ReactNode;
5 | };
6 |
7 | const Counter: React.SFC = ({ children }) => {
8 | const [value, update] = React.useState(0);
9 | return (
10 |
11 | Count: {value}
12 | {children(value)}
13 | update(v => v + 1)}>+1
14 | update(v => v - 1)}>-1
15 |
16 | );
17 | };
18 |
19 | export default Counter;
20 |
--------------------------------------------------------------------------------
/examples/02_typescript/src/DisplayNumber.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { useRenderProps, wrap } from 'react-hooks-render-props';
4 |
5 | import Counter from './Counter';
6 |
7 | const DisplayNumber = wrap(() => {
8 | const [value] = useRenderProps<{}, number>(Counter);
9 | return Number: {value}
;
10 | });
11 |
12 | export default DisplayNumber;
13 |
--------------------------------------------------------------------------------
/examples/02_typescript/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import App from './App';
5 |
6 | render(React.createElement(App), document.getElementById('app'));
7 |
--------------------------------------------------------------------------------
/examples/03_apollo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-hooks-render-props-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/react": "^16.7.6",
7 | "@types/react-dom": "^16.0.9",
8 | "apollo-cache-inmemory": "^1.3.8",
9 | "apollo-client": "^2.4.5",
10 | "apollo-link-schema": "^1.1.1",
11 | "graphql": "^14.0.2",
12 | "graphql-tag": "^2.10.0",
13 | "graphql-tools": "^4.0.3",
14 | "react": "latest",
15 | "react-apollo": "^2.2.4",
16 | "react-dom": "latest",
17 | "react-hooks-render-props": "latest",
18 | "react-scripts": "^2.1.1",
19 | "typescript": "^3.1.6"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "browserslist": [
28 | ">0.2%",
29 | "not dead",
30 | "not ie <= 11",
31 | "not op_mini all"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/examples/03_apollo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | react-hooks-render-props example
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/03_apollo/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { InMemoryCache } from 'apollo-cache-inmemory';
4 | import { ApolloClient } from 'apollo-client';
5 | import { ApolloProvider } from 'react-apollo';
6 |
7 | import { mockedLink } from './mock';
8 | import NewPost from './NewPost';
9 | import PostList from './PostList';
10 |
11 | const client = new ApolloClient({
12 | cache: new InMemoryCache(),
13 | link: mockedLink,
14 | });
15 |
16 | const App = () => (
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/examples/03_apollo/src/NewPost.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import graphqlTag from 'graphql-tag';
4 | import { Mutation, MutationFn, MutationResult } from 'react-apollo';
5 |
6 | import { useRenderProps, wrap } from 'react-hooks-render-props';
7 |
8 | type Props = {
9 | add: (text: string) => void;
10 | };
11 |
12 | const TextInput: React.SFC = ({ add }) => {
13 | const [text, setText] = React.useState('');
14 | const onSubmit = (event: React.FormEvent) => {
15 | event.preventDefault();
16 | add(text);
17 | setText('');
18 | };
19 | return (
20 |
27 | );
28 | };
29 |
30 | const ADD_POST = graphqlTag`
31 | mutation addPost($text: String!) {
32 | addPost(text: $text)
33 | }
34 | `;
35 |
36 | const useApolloMutation = (props: {}): [MutationFn, MutationResult] => {
37 | // @ts-ignore: FIXME not assignable
38 | const results = useRenderProps<{}, MutationResult>(Mutation, props);
39 | return results;
40 | };
41 |
42 | const NewPost = wrap(() => {
43 | const [addPost] = useApolloMutation({ mutation: ADD_POST, refetchQueries: ['queryPosts'] });
44 | const add = (text: string) => {
45 | addPost({ variables: { text } });
46 | };
47 | return (
48 |
49 | );
50 | });
51 |
52 | export default NewPost;
53 |
--------------------------------------------------------------------------------
/examples/03_apollo/src/PostList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { DocumentNode } from 'graphql';
4 | import graphqlTag from 'graphql-tag';
5 | import { OperationVariables, Query, QueryResult } from 'react-apollo';
6 |
7 | import { useRenderProps, wrap } from 'react-hooks-render-props';
8 |
9 | const QUERY_POSTS = graphqlTag`
10 | query queryPosts {
11 | posts {
12 | id
13 | text
14 | }
15 | }
16 | `;
17 |
18 | type DataItem = {
19 | text: string;
20 | id: number;
21 | };
22 |
23 | type Data = {
24 | posts: DataItem[];
25 | };
26 |
27 | class QueryPosts extends Query {}
28 |
29 | type Result = QueryResult;
30 |
31 | const useApolloQuery = (query: string): Result => {
32 | // @ts-ignore: FIXME not assignable
33 | const [result] = useRenderProps<{ query: DocumentNode }, Result>(QueryPosts, { query });
34 | const fallbackResult = { loading: true }; // XXX this is a limitation.
35 | return result || fallbackResult;
36 | };
37 |
38 | const PostList: React.SFC = wrap(() => {
39 | const { loading, error, data } = useApolloQuery(QUERY_POSTS);
40 | if (loading) return Loading... ;
41 | if (error) return Error: {error} ;
42 | if (!data) return No Data ;
43 | return (
44 |
45 | {data.posts.map(item => {item.text} )}
46 |
47 | );
48 | });
49 |
50 | export default PostList;
51 |
--------------------------------------------------------------------------------
/examples/03_apollo/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import App from './App';
5 |
6 | render(React.createElement(App), document.getElementById('app'));
7 |
--------------------------------------------------------------------------------
/examples/03_apollo/src/mock.ts:
--------------------------------------------------------------------------------
1 | import { SchemaLink } from 'apollo-link-schema';
2 | import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools';
3 |
4 | const typeDefs = `
5 | type Query {
6 | posts: [Post]
7 | }
8 | type Post {
9 | id: ID!,
10 | text: String!,
11 | }
12 | type Mutation {
13 | addPost(text: String!): Post
14 | }
15 | `;
16 |
17 | const posts = [{
18 | id: 1001,
19 | text: 'This is the first post',
20 | }, {
21 | id: 1002,
22 | text: 'This is the second',
23 | }];
24 |
25 | const mocks = {
26 | Mutation: () => ({
27 | addPost: (_root: unknown, { text }: { text: string }) => posts.push({ text, id: Date.now() }),
28 | }),
29 | Query: () => ({
30 | posts: () => posts,
31 | }),
32 | };
33 |
34 | const schema = makeExecutableSchema({ typeDefs });
35 | addMockFunctionsToSchema({ mocks, schema });
36 |
37 | export const mockedLink = new SchemaLink({ schema });
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-hooks-render-props",
3 | "description": "A hacking custom hook to emulate render props (function as a child)",
4 | "version": "0.2.0",
5 | "author": "Daishi Kato",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/dai-shi/react-hooks-render-props.git"
9 | },
10 | "main": "./dist/index.js",
11 | "module": "./src/index.js",
12 | "types": "./src/index.d.ts",
13 | "files": [
14 | "src",
15 | "dist"
16 | ],
17 | "scripts": {
18 | "compile": "babel src -d dist",
19 | "test": "npm run eslint && npm run tsc-test && npm run jest",
20 | "eslint": "eslint --ext .js,.ts,.tsx --ignore-pattern dist .",
21 | "jest": "jest",
22 | "tsc-test": "tsc --project . --noEmit",
23 | "examples:minimal": "DIR=01_minimal EXT=js webpack-dev-server",
24 | "examples:typescript": "DIR=02_typescript webpack-dev-server",
25 | "examples:apollo": "DIR=03_apollo webpack-dev-server"
26 | },
27 | "keywords": [
28 | "react",
29 | "hooks",
30 | "stateless",
31 | "thisless",
32 | "pure"
33 | ],
34 | "license": "MIT",
35 | "dependencies": {},
36 | "devDependencies": {
37 | "@babel/cli": "^7.2.3",
38 | "@babel/core": "^7.2.2",
39 | "@babel/preset-env": "^7.3.1",
40 | "@babel/preset-react": "^7.0.0",
41 | "@types/graphql": "^14.0.5",
42 | "@types/react": "^16.8.2",
43 | "@types/react-dom": "^16.8.0",
44 | "@typescript-eslint/eslint-plugin": "^1.3.0",
45 | "apollo-cache-inmemory": "^1.4.2",
46 | "apollo-client": "^2.4.12",
47 | "apollo-link-schema": "^1.1.6",
48 | "babel-loader": "^8.0.5",
49 | "eslint": "^5.13.0",
50 | "eslint-config-airbnb": "^17.1.0",
51 | "eslint-plugin-import": "^2.16.0",
52 | "eslint-plugin-jsx-a11y": "^6.2.1",
53 | "eslint-plugin-react": "^7.12.4",
54 | "eslint-plugin-react-hooks": "^1.5.1",
55 | "graphql": "^14.1.1",
56 | "graphql-tag": "^2.10.1",
57 | "graphql-tools": "^4.0.4",
58 | "html-webpack-plugin": "^3.2.0",
59 | "jest": "^24.1.0",
60 | "react": "16.8.1",
61 | "react-apollo": "^2.4.1",
62 | "react-dom": "16.8.1",
63 | "react-testing-library": "^5.5.3",
64 | "ts-loader": "^5.3.3",
65 | "typescript": "^3.3.3",
66 | "webpack": "^4.29.3",
67 | "webpack-cli": "^3.2.3",
68 | "webpack-dev-server": "^3.1.14"
69 | },
70 | "peerDependencies": {
71 | "react": ">=16.8.0"
72 | },
73 | "babel": {
74 | "presets": [
75 | "@babel/preset-env",
76 | "@babel/preset-react"
77 | ]
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | type Children = ((...args: T[]) => React.ReactNode);
4 |
5 | export const useRenderProps: (
6 | c: React.ComponentType
}>,
7 | p?: P,
8 | ) => T[];
9 | export const wrap:
(c: React.SFC
) => React.SFC
;
10 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | createElement,
3 | useEffect,
4 | useRef,
5 | useState,
6 | } from 'react';
7 |
8 | let sharedObject = null;
9 |
10 | export const useRenderProps = (WrapperComponent, wrapperProps) => {
11 | const [args, setArgs] = useState([]);
12 | const updateFlag = useRef(true);
13 | sharedObject = {
14 | setArgs,
15 | updateFlag,
16 | WrapperComponent,
17 | wrapperProps,
18 | };
19 | useEffect(() => {
20 | updateFlag.current = !updateFlag.current;
21 | });
22 | return args;
23 | };
24 |
25 | export const wrap = FunctionComponent => (props) => {
26 | sharedObject = null;
27 | const element = FunctionComponent(props);
28 | const {
29 | setArgs,
30 | updateFlag,
31 | WrapperComponent,
32 | wrapperProps,
33 | } = sharedObject || {};
34 | if (!WrapperComponent) return element;
35 | return createElement(WrapperComponent, wrapperProps, (...args) => {
36 | if (updateFlag.current) {
37 | setArgs(args);
38 | }
39 | return element;
40 | });
41 | };
42 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "lib": ["es2015", "esnext.asynciterable", "dom"],
5 | "jsx": "react",
6 | "noUnusedLocals": true,
7 | "noUnusedParameters": true,
8 | "baseUrl": ".",
9 | "paths": {
10 | "react-hooks-render-props": ["."]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | const { DIR, EXT = 'ts' } = process.env;
5 |
6 | module.exports = {
7 | mode: 'development',
8 | entry: `./examples/${DIR}/src/index.${EXT}`,
9 | plugins: [
10 | new HtmlWebpackPlugin({
11 | template: `./examples/${DIR}/public/index.html`,
12 | }),
13 | ],
14 | module: {
15 | rules: [{
16 | test: /\.jsx?$/,
17 | use: [{
18 | loader: 'babel-loader',
19 | options: {
20 | presets: [
21 | '@babel/preset-env',
22 | '@babel/preset-react',
23 | ],
24 | },
25 | }],
26 | }, {
27 | test: /\.tsx?$/,
28 | loader: 'ts-loader',
29 | }],
30 | },
31 | resolve: {
32 | extensions: ['.mjs', '.js', '.jsx', '.ts', '.tsx'],
33 | alias: {
34 | 'react-hooks-render-props': __dirname,
35 | },
36 | },
37 | devServer: {
38 | port: process.env.PORT || '8080',
39 | },
40 | };
41 |
--------------------------------------------------------------------------------