├── .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 | [![Build Status](https://travis-ci.com/dai-shi/react-hooks-render-props.svg?branch=master)](https://travis-ci.com/dai-shi/react-hooks-render-props) 4 | [![npm version](https://badge.fury.io/js/react-hooks-render-props.svg)](https://badge.fury.io/js/react-hooks-render-props) 5 | [![bundle size](https://badgen.net/bundlephobia/minzip/react-hooks-render-props)](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 | 14 | 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 |
21 | setText(event.target.value)} 24 | value={text} 25 | /> 26 |
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 | --------------------------------------------------------------------------------