├── .librc ├── .travis.yml ├── utils └── test-setup.ts ├── .prettierrc ├── .editorconfig ├── CONTRIBUTING.md ├── .gitignore ├── tslint.json ├── LICENSE ├── tsconfig.json ├── package.json ├── src ├── index.tsx └── index.test.tsx └── README.md /.librc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["**/**.test.js"] 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | branches: 4 | only: 5 | - master 6 | -------------------------------------------------------------------------------- /utils/test-setup.ts: -------------------------------------------------------------------------------- 1 | import * as enzyme from 'enzyme' 2 | import * as Adapter from 'enzyme-adapter-react-16' 3 | 4 | enzyme.configure({ adapter: new Adapter() }) 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "requirePragma": false, 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "semi": false, 8 | "singleQuote": true, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [package.json] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | ## 🕺   Contribute 3 | 4 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device 5 | 2. Install dependencies using Yarn: `yarn install` 6 | 3. Make the necessary changes and ensure that the tests are passing using `yarn test` 7 | 4. Send a pull request 🙌 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pids 7 | *.pid 8 | *.seed 9 | *.pid.lock 10 | lib-cov 11 | coverage 12 | .nyc_output 13 | .grunt 14 | bower_components 15 | .lock-wscript 16 | build/Release 17 | node_modules/ 18 | jspm_packages/ 19 | typings/ 20 | .npm 21 | .npmrc 22 | .eslintcache 23 | .node_repl_history 24 | *.tgz 25 | .yarn-integrity 26 | .env 27 | es 28 | lib 29 | stats.html 30 | build 31 | dist 32 | .rpt2_cache 33 | .cache 34 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name": [true, "never-prefix"], 5 | "ordered-imports": false, 6 | "object-literal-sort-keys": false, 7 | 8 | // waiting on https://github.com/palantir/tslint/pull/3708 9 | "no-implicit-dependencies": [true, "dev"], 10 | "no-duplicate-imports": false, 11 | "prefer-object-spread": false, 12 | "no-console": false, 13 | "no-shadowed-variable": false, 14 | "max-classes-per-file": false, 15 | 16 | // Recommended built-in rules 17 | "no-var-keyword": true, 18 | "no-parameter-reassignment": true, 19 | "typedef": [true, "call-signature"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pedro Nauck 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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "inlineSourceMap": true, 8 | 9 | "jsx": "react", 10 | "strict": true /* Enable all strict type-checking options. */, 11 | 12 | /* Strict Type-Checking Options */ 13 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 14 | "strictNullChecks": true /* Enable strict null checks. */, 15 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 16 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 17 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 18 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 19 | 20 | /* Additional Checks */ 21 | "noUnusedLocals": true /* Report errors on unused locals. */, 22 | "noUnusedParameters": false /* Report errors on unused parameters. */, 23 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 24 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 25 | 26 | /* Debugging Options */ 27 | "traceResolution": false /* Report module resolution log messages. */, 28 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 29 | "listFiles": false /* Print names of files part of the compilation. */, 30 | "pretty": true /* Stylize errors and messages using color and context. */, 31 | "declaration": true, 32 | 33 | "lib": ["es2017", "dom"], 34 | "outDir": "dist", 35 | "rootDir": "src", 36 | "typeRoots": ["node_modules/@types"] 37 | }, 38 | "include": ["src/**.tsx"], 39 | "exclude": ["node_modules/**"] 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-adopt", 3 | "version": "0.6.0", 4 | "main": "dist/index.js", 5 | "umd:main": "dist/index.umd.js", 6 | "module": "dist/index.m.js", 7 | "types": "dist/index.d.ts", 8 | "source": "src/index.tsx", 9 | "license": "MIT", 10 | "repository": "pedronauck/react-adopt", 11 | "homepage": "https://github.com/pedronauck/react-adopt", 12 | "bugs": "https://github.com/pedronauck/react-adopt/issues", 13 | "author": "Pedro Nauck ", 14 | "scripts": { 15 | "dev": "libundler watch --ts", 16 | "build": "libundler build --ts --sourcemap", 17 | "build:prod": "yarn build --compress", 18 | "prebuild:prod": "yarn run fix", 19 | "test": "cross-env NODE_ENV=test jest", 20 | "fix": "run-s fix:*", 21 | "fix:prettier": "prettier \"src/**/*.{ts,tsx}\" --write", 22 | "fix:tslint": "tslint -p ./", 23 | "release": "np", 24 | "prerelease": "yarn build:prod" 25 | }, 26 | "files": [ 27 | "dist/", 28 | "README.md", 29 | "LICENSE" 30 | ], 31 | "keywords": [ 32 | "react", 33 | "render-props", 34 | "compose" 35 | ], 36 | "dependencies": { 37 | "hoist-non-react-statics": "^2.5.0", 38 | "react": "^16.3.2", 39 | "react-display-name": "^0.2.4" 40 | }, 41 | "peerDependencies": { 42 | "react": ">= 0.14.0" 43 | }, 44 | "devDependencies": { 45 | "@types/enzyme": "^3.1.10", 46 | "@types/enzyme-adapter-react-16": "^1.0.2", 47 | "@types/jest": "^22.2.3", 48 | "@types/react": "^16.3.14", 49 | "cross-env": "^5.1.5", 50 | "enzyme": "^3.3.0", 51 | "enzyme-adapter-react-16": "^1.1.1", 52 | "husky": "^0.14.3", 53 | "jest": "^22.4.3", 54 | "libundler": "^1.6.4", 55 | "lint-staged": "^7.1.0", 56 | "np": "^2.20.1", 57 | "npm-run-all": "^4.1.3", 58 | "prettier": "^1.12.1", 59 | "react-dom": "^16.3.2", 60 | "react-powerplug": "^0.1.5", 61 | "ts-jest": "^22.4.5", 62 | "ts-node": "^6.0.3", 63 | "tslint": "^5.10.0", 64 | "tslint-config-prettier": "^1.12.0", 65 | "typescript": "^2.8.3" 66 | }, 67 | "jest": { 68 | "setupFiles": [ 69 | "/utils/test-setup.ts" 70 | ], 71 | "transform": { 72 | "^.+\\.(ts|tsx)$": "ts-jest" 73 | }, 74 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(js?|jsx?|jsx?|tsx?)$", 75 | "moduleFileExtensions": [ 76 | "ts", 77 | "tsx", 78 | "js", 79 | "jsx", 80 | "json", 81 | "node" 82 | ] 83 | }, 84 | "lint-staged": { 85 | "src/**.tsx": [ 86 | "yarn fix:prettier", 87 | "git add" 88 | ] 89 | }, 90 | "husky": { 91 | "pre-commit": "lint-staged && yarn test" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { ReactNode, ReactElement } from 'react' 3 | import * as hoistNonReactStatic from 'hoist-non-react-statics' 4 | import getDisplayName from 'react-display-name' 5 | 6 | const { values, keys, assign } = Object 7 | 8 | export declare type ChildrenFn

= (props: P) => JSX.Element | null 9 | 10 | function omit(omitProps: string[], obj: any): R { 11 | const newObj = keys(obj) 12 | .filter((key: string): boolean => omitProps.indexOf(key) === -1) 13 | .reduce( 14 | (returnObj: any, key: string): R => ({ ...returnObj, [key]: obj[key] }), 15 | {} 16 | ) 17 | 18 | return newObj as R 19 | } 20 | 21 | function prop(key: string, obj: any): T { 22 | return obj[key] 23 | } 24 | 25 | const isFn = (val: any): boolean => Boolean(val) && typeof val === 'function' 26 | 27 | const isValidRenderProp = (prop: ReactNode | ChildrenFn): boolean => 28 | React.isValidElement(prop) || isFn(prop) 29 | 30 | export declare type RPC = React.ComponentType< 31 | P & { 32 | children?: ChildrenFn 33 | render?: ChildrenFn 34 | } 35 | > 36 | 37 | export declare type MapperComponent = React.ComponentType< 38 | RP & 39 | P & { 40 | render?: ChildrenFn 41 | } 42 | > 43 | 44 | export declare type MapperValue = 45 | | ReactElement 46 | | MapperComponent 47 | 48 | export declare type Mapper = Record> 49 | 50 | export declare type MapProps = (props: any) => RP 51 | 52 | export function adopt( 53 | mapper: Mapper, 54 | mapProps?: MapProps 55 | ): RPC { 56 | if (!values(mapper).some(isValidRenderProp)) { 57 | throw new Error( 58 | 'The render props object mapper just accept valid elements as value' 59 | ) 60 | } 61 | 62 | const mapperKeys = keys(mapper) 63 | const Children: any = ({ render, children, ...rest }: any) => 64 | render && isFn(render) 65 | ? render(rest) 66 | : children && isFn(children) && children(rest) 67 | 68 | Children.displayName = 'Adopt' 69 | 70 | const reducer = (Component: RPC, key: string, idx: number): RPC => { 71 | const element = prop(key, mapper) 72 | const displayName = getDisplayName(Component) 73 | const nextDisplayName = getDisplayName(element) 74 | const isLast = idx === mapperKeys.length - 1 75 | 76 | const NewComponent: RPC = ({ 77 | render: pRender, 78 | children, 79 | ...rest 80 | }: any) => ( 81 | 82 | {props => { 83 | const propsWithoutRest = omit(keys(rest), props) 84 | const render = pRender && isFn(pRender) ? pRender : children 85 | 86 | const renderFn: ChildrenFn = cProps => { 87 | const renderProps = assign({}, propsWithoutRest, { 88 | [key]: cProps, 89 | }) 90 | 91 | const propsToPass = 92 | mapProps && isFn(mapProps) && isLast 93 | ? mapProps(renderProps) 94 | : renderProps 95 | 96 | return render && isFn(render) ? render(propsToPass) : null 97 | } 98 | 99 | return isFn(element) 100 | ? React.createElement( 101 | element, 102 | assign({}, rest, props, { render: renderFn }) 103 | ) 104 | : React.cloneElement(element, {}, renderFn) 105 | }} 106 | 107 | ) 108 | 109 | NewComponent.displayName = `${displayName}(${nextDisplayName})` 110 | 111 | return isFn(element) 112 | ? hoistNonReactStatic(NewComponent, element) 113 | : NewComponent 114 | } 115 | 116 | return mapperKeys.reduce(reducer, Children) 117 | } 118 | 119 | export type AdoptProps = P & { 120 | mapper: Mapper 121 | children?: ChildrenFn 122 | render?: ChildrenFn 123 | mapProps?: MapProps 124 | } 125 | 126 | export class Adopt extends React.Component> { 127 | private Composed: React.ComponentType 128 | 129 | constructor(props: any) { 130 | super(props) 131 | this.Composed = adopt(props.mapper, this.props.mapProps) 132 | } 133 | 134 | public render(): JSX.Element { 135 | const { mapper, mapProps, ...props } = this.props 136 | return 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow, mount } from 'enzyme' 3 | import { Value } from 'react-powerplug' 4 | 5 | import { adopt, Adopt } from './' 6 | 7 | test('return one component with children props as function', () => { 8 | interface RenderProps { 9 | foo: { value: string } 10 | } 11 | 12 | const Composed = adopt({ 13 | foo: , 14 | }) 15 | 16 | const result = mount( 17 | {({ foo }: any) =>

{foo.value}
} 18 | ) 19 | const { children } = result.props() 20 | 21 | expect(children).toBeDefined() 22 | expect(typeof children).toBe('function') 23 | }) 24 | 25 | test('rendering children component', () => { 26 | interface RenderProps { 27 | foo: { value: string } 28 | bar: { value: string } 29 | } 30 | 31 | interface Props { 32 | tor: string 33 | } 34 | 35 | const Composed = adopt({ 36 | foo: ({ tor, render }) => {render}, 37 | bar: ({ tor, render }) => {render}, 38 | }) 39 | 40 | const result = shallow( 41 | 42 | {({ foo, bar }) => ( 43 |
44 |
{foo.value}
45 |
{bar.value}
46 |
47 | )} 48 |
49 | ) 50 | 51 | expect(result.children().length).toBe(1) 52 | expect(result.html()).toBe('
torfoo
torbar
') 53 | }) 54 | 55 | test('using a render prop on composed component', () => { 56 | const Composed = adopt({ 57 | foo: , 58 | }) 59 | 60 | const result = shallow( 61 |
{foo.value}
} /> 62 | ) 63 | 64 | expect(result.children().length).toBe(1) 65 | expect(result.html()).toBe('
foo
') 66 | }) 67 | 68 | test('passing a function', () => { 69 | const Foo = ({ children }: any) => children('foo') 70 | const foo = jest.fn(({ render }) => {render}) 71 | const children = jest.fn(() => null) 72 | const Composed = adopt({ foo }) 73 | 74 | mount({children}) 75 | 76 | expect(foo).toHaveBeenCalled() 77 | expect(children).toHaveBeenCalledWith({ foo: 'foo' }) 78 | }) 79 | 80 | test('passing a function changing the render prop on mapper', () => { 81 | const Foo = ({ render }: any) => render('foo') 82 | 83 | const foo = jest.fn(({ render }) => ) 84 | const children = jest.fn(() => null) 85 | const Composed = adopt({ foo }) 86 | 87 | mount({children}) 88 | 89 | expect(foo).toHaveBeenCalled() 90 | expect(children).toHaveBeenCalledWith({ foo: 'foo' }) 91 | }) 92 | 93 | test('should provide a function mapper with all previous render prop results', () => { 94 | const Foo = ({ children }: any) => children('foo') 95 | const Bar = ({ children }: any) => children('bar') 96 | const bar = jest.fn(({ render }) => {render}) 97 | const children = jest.fn(() => null) 98 | 99 | interface RenderProps { 100 | foo: 'foo' 101 | bar: 'bar' 102 | } 103 | 104 | const Composed = adopt({ 105 | foo: , 106 | bar, 107 | }) 108 | 109 | mount({children}) 110 | 111 | expect(bar.mock.calls[0][0]).toHaveProperty('foo', 'foo') 112 | expect(children).toHaveBeenCalledWith({ foo: 'foo', bar: 'bar' }) 113 | }) 114 | 115 | test('should provide mapper functions with Composed component props', () => { 116 | const Foo = ({ children }: any) => children('foo') 117 | const foo = jest.fn(({ render }) => {render}) 118 | const children = jest.fn(() => null) 119 | 120 | interface RenderProps { 121 | foo: string 122 | } 123 | 124 | interface Props { 125 | bar: string 126 | } 127 | 128 | const Composed = adopt({ 129 | foo, 130 | }) 131 | 132 | mount({children}) 133 | 134 | expect(foo.mock.calls[0][0]).toHaveProperty('bar', 'bar') 135 | expect(children).toHaveBeenCalledWith({ foo: 'foo' }) 136 | }) 137 | 138 | test('throw with a wrong value on mapper', () => { 139 | expect(() => { 140 | const Composed = adopt({ foo: 'helo' } as any) 141 | return shallow({(props: any) =>
foo
}
) 142 | }).toThrowError( 143 | 'The render props object mapper just accept valid elements as value' 144 | ) 145 | }) 146 | 147 | test('inline composition using component', () => { 148 | const Foo = ({ children }: any) => children('foo') 149 | const children = jest.fn(({ foo }) =>
{foo}
) 150 | 151 | const mapper = { 152 | foo: , 153 | } 154 | 155 | const element = {children} 156 | 157 | mount(element) 158 | expect(children).toHaveBeenCalledWith({ foo: 'foo' }) 159 | 160 | const result = shallow(element) 161 | 162 | expect(result.children().length).toBe(1) 163 | expect(result.html()).toBe('
foo
') 164 | }) 165 | 166 | test('mapping props as second parameter of adopt()', () => { 167 | const children = jest.fn(() => null) 168 | 169 | const Composed = adopt( 170 | { 171 | foo: , 172 | bar: , 173 | }, 174 | ({ foo, bar }) => ({ 175 | foobar: foo.value + bar.value, 176 | }) 177 | ) 178 | 179 | mount({children}) 180 | 181 | expect(children).toHaveBeenCalledWith({ foobar: 'foobar' }) 182 | }) 183 | 184 | test('mapping props as prop of ', () => { 185 | const children = jest.fn(() => null) 186 | 187 | const mapper = { 188 | foo: , 189 | bar: , 190 | } 191 | 192 | const mapProps = ({ foo, bar }: any) => ({ 193 | foobar: foo.value + bar.value, 194 | }) 195 | 196 | mount( 197 | 198 | {children} 199 | 200 | ) 201 | 202 | expect(children).toHaveBeenCalledWith({ foobar: 'foobar' }) 203 | }) 204 | 205 | test('hoisting non-static react methods from mapper values', () => { 206 | interface GreeterProps { 207 | name: string 208 | render?: (name: string) => JSX.Element 209 | } 210 | 211 | class Greeter extends React.Component { 212 | public static sayHello = (name: string): string => `Hello ${name}` 213 | 214 | public render(): any { 215 | const { render } = this.props 216 | return render && typeof render === 'function' && render(`Hello John`) 217 | } 218 | } 219 | 220 | const getHelloFromStatic = jest.fn((value: string) => value) 221 | const children = jest.fn(() => null) 222 | 223 | const Composed: any = adopt({ 224 | name: ({ render }) => render('John'), 225 | greeter: Greeter, 226 | }) 227 | 228 | mount({children}) 229 | getHelloFromStatic(Composed.sayHello('John')) 230 | 231 | expect(getHelloFromStatic).toHaveBeenCalledWith('Hello John') 232 | expect(children).toHaveBeenCalledWith({ greeter: 'Hello John', name: 'John' }) 233 | }) 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :sunglasses: _**React Adopt -**_ Compose render props components like a pro 2 | 3 | [![GitHub release](https://img.shields.io/github/release/pedronauck/react-adopt.svg)]() 4 | [![Build Status](https://travis-ci.org/pedronauck/react-adopt.svg?branch=master)](https://travis-ci.org/pedronauck/react-adopt) 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/ebdcc3e942b14363a96438b41c770b32)](https://www.codacy.com/app/pedronauck/react-adopt?utm_source=github.com&utm_medium=referral&utm_content=pedronauck/react-adopt&utm_campaign=Badge_Grade) 6 | 7 | ![](https://i.imgflip.com/27euu2.jpg) 8 | 9 | ## 📜 Table of content 10 | 11 | - [Why](#--why) 12 | - [Solution](#--solution) 13 | - [Demos](#--demos) 14 | - [Usage](#--usage-demo) 15 | - [Working with new Context api](#working-with-new-context-api) 16 | - [Custom render and retrieving props from composed](#custom-render-and-retrieving-props-from-composed) 17 | - [Mapping props from mapper](#mapping-props-from-mapper) 18 | - [Using components on mapper](#using-components-on-mapper) 19 | - [Leading with multiple params](#leading-with-multiple-params) 20 | - [Typescript support](#typescript-support) 21 | - [Inline composition](#inline-composition) 22 | 23 | ## 🧐   Why 24 | 25 | [Render Props](https://reactjs.org/docs/render-props.html) are the new hype of React's ecosystem, that's a fact. So, when you need to use more than one render props component together, this can be boring and generate something called a *"render props callback hell"*, like this: 26 | 27 | ![Bad](https://i.imgur.com/qmk3Bk5.png) 28 | 29 | ## 💡   Solution 30 | 31 | * **Small**. 0.7kb minified! 32 | * **Extremely Simple**. Just a method! 33 | 34 | React Adopt is a simple method that composes multiple render prop components, combining each prop result from your mapper. 35 | 36 | ## 📟   Demos 37 | 38 | - [Basic example](https://codesandbox.io/s/vq1wl37m0y?hidenavigation=1) 39 | - [Todo App example using React Apollo](https://codesandbox.io/s/3x7n8wyp15?hidenavigation=1) 40 | - [Example with new Context API](https://codesandbox.io/s/qv3m6yk2n4?hidenavigation=1) 41 | 42 | ## 💻   Usage 43 | 44 | Install as project dependency: 45 | 46 | ```bash 47 | $ yarn add react-adopt 48 | ``` 49 | 50 | Now you can use React Adopt to compose your components. See below for an example using the awesome [react-powerplug](https://github.com/renatorib/react-powerplug): 51 | 52 | ![Good](https://i.imgur.com/RXVlFwy.png) 53 | 54 | ### Working with new Context api 55 | 56 | One use case that React Adopt can fit perfectly is when you need to use [React's new context api](https://reactjs.org/docs/context.html) that use render props to create some context: 57 | 58 | ```jsx 59 | import React from 'react' 60 | import { adopt } from 'react-adopt' 61 | 62 | const ThemeContext = React.createContext('light') 63 | const UserContext = React.createContext({ name: 'John' }) 64 | 65 | const Context = adopt({ 66 | theme: , 67 | user: , 68 | }) 69 | 70 | 71 | {({ theme, user }) => /* ... */} 72 | 73 | ``` 74 | 75 | See [this demo](https://codesandbox.io/s/qv3m6yk2n4?hidenavigation=1) for a better explanation. 76 | 77 | ### Custom render and retrieving props from composed 78 | 79 | Some components don't use the `children` prop for render props to work. For cases like this, you can pass a function instead of a jsx element to your mapper. This function will receive a `render` prop that will be responsible for your render, the props passed on `Composed` component, and the previous values from each mapper. See an example: 80 | 81 | ```jsx 82 | import { adopt } from 'react-adopt' 83 | import MyCustomRenderProps from 'my-custom-render-props' 84 | 85 | const Composed = adopt({ 86 | custom: ({ render }) => 87 | }) 88 | 89 | 90 | {({ custom }) => ( 91 |
{custom.value}
92 | )} 93 |
94 | ``` 95 | 96 | You can also retrieve the properties passed to the composed component this way too: 97 | 98 | 99 | ```jsx 100 | import { adopt } from 'react-adopt' 101 | import { Value } from 'react-powerplug' 102 | 103 | const Composed = adopt({ 104 | greet: ({ initialGreet, render }) => ( 105 | {render} 106 | ) 107 | }) 108 | 109 | 110 | {({ greet }) => ( 111 |
{greet.value}
112 | )} 113 |
114 | ``` 115 | 116 | And get previous mapper results as prop for compose: 117 | 118 | ```jsx 119 | import { adopt } from 'react-adopt' 120 | 121 | import { User, Cart, ShippingRate } from 'my-containers' 122 | 123 | const Composed = adopt({ 124 | cart: , 125 | user: , 126 | shippingRates: ({ user, cart, render }) => ( 127 |     128 | {render} 129 | 130 | ) 131 | }) 132 | 133 | 134 | {({ cart, user, shippingRates }) => /* ... */ } 135 | 136 | ``` 137 | 138 | ### Mapping props from mapper 139 | 140 | Sometimes, get properties from your mappers can be kind of boring depending on how nested the result from each mapper. To easily avoid deeply nested objects or combine your results, you can map the final results into a single object using the `mapProps` function as the second parameter. 141 | 142 | ```js 143 | import { adopt } from 'react-adopt' 144 | import { Value } from 'react-powerplug' 145 | 146 | const mapper = { 147 | greet: , 148 | name: , 149 | } 150 | 151 | const mapProps = ({ greet, name }) => ({ 152 | message: `${greet.value} ${name.value}`, 153 | }) 154 | 155 | const Composed = adopt(mapper, mapProps) 156 | 157 | const App = () => ( 158 | 159 | {({ message }) => /* ... */} 160 | 161 | ) 162 | ``` 163 | 164 | You can do that using the `` component as well: 165 | 166 | ```js 167 | import { Adopt } from 'react-adopt' 168 | import { Value } from 'react-powerplug' 169 | 170 | const mapper = { 171 | greet: , 172 | name: , 173 | } 174 | 175 | const mapProps = ({ greet, name }) => ({ 176 | message: `${greet.value} ${name.value}`, 177 | }) 178 | 179 | const App = () => ( 180 | 181 | {({ message }) => /* ... */} 182 | 183 | ) 184 | ``` 185 | 186 | ### Using components on mapper 187 | 188 | If you want to use your component directly as mapper value you can do that. Some nice thing here is that all non-react static methods of your components will be hoisting for composed component: 189 | 190 | ```jsx 191 | import { React } from 'react' 192 | import { adopt } from 'adopt' 193 | import { Value } from 'react-powerplug' 194 | 195 | const Greeter = ({ render, name }) => render(`Hi ${name.value}`) 196 | 197 | Greeter.sayHi = (name) => `Hi ${name}` 198 | 199 | const Composed = adopt({ 200 | name: 201 | greet: Greet 202 | }) 203 | 204 | console.log(Composed.sayHi('John')) // Hi John 205 | 206 | const App = () => ( 207 | 208 | {({ greet, name }) => /* ... */ } 209 | 210 | ) 211 | ``` 212 | 213 | ### Leading with multiple params 214 | 215 | Some render props components return multiple arguments in the children function instead of a single one (see a simple example in the new [Query](https://www.apollographql.com/docs/react/essentials/queries.html#basic) and [Mutation](https://www.apollographql.com/docs/react/essentials/mutations.html) component from `react-apollo`). In this case, you can do an arbitrary render with `render` prop [using your map value as a function](#custom-render-and-retrieving-props-from-composed): 216 | 217 | ```js 218 | import { adopt } from 'react-adopt' 219 | import { Mutation } from 'react-apollo' 220 | 221 | const ADD_TODO = /* ... */ 222 | 223 | const addTodo = ({ render }) => ( 224 | 225 | {/* this is an arbitrary render where you will pass your two arguments into a single one */} 226 |    {(mutation, result) => render({ mutation, result })} 227 |   228 | ) 229 | 230 | const Composed = adopt({ 231 | addTodo, 232 | }) 233 | 234 | const App = () => ( 235 | 236 | {({ addTodo: { mutation, result } }) => /* ... */} 237 | 238 | ) 239 | ``` 240 | 241 | See [this demo](https://codesandbox.io/s/3x7n8wyp15?hidenavigation=1) for a complete explanation about multiple params.. 242 | 243 | ### Typescript support 244 | 245 | React Adopt has full typescript support when you need to type the composed component: 246 | 247 | ```ts 248 | import * as React from 'react' 249 | import { adopt } from 'react-adopt' 250 | import { Value } from 'react-powerplug' 251 | 252 | interface RenderProps { 253 | foo: { value: string } 254 | } 255 | 256 | interface Props { 257 | tor: string 258 | } 259 | 260 | const foo = ({ tor, render }) => ( 261 | {render} 262 | ) 263 | 264 | const Composed = adopt({ 265 | foo, 266 | }) 267 | 268 | 269 | {({ foo, bar }) => ( 270 |
{foo.value}
271 | )} 272 |
273 | ``` 274 | 275 | ### Inline composition 276 | 277 | If you dont care about [typings](#typescript-support) and need something more easy and quick, you can choose to use an inline composition by importing the `` component and passing your mapper as a prop: 278 | 279 | ```js 280 | import React from 'react' 281 | import { Adopt } from 'react-adopt' 282 | import { Value } from 'react-powerplug' 283 | 284 | const mapper = { 285 | greet: , 286 | name: 287 | } 288 | 289 | 290 | {({ greet, name }) => /* ... */} 291 | 292 | ``` 293 | 294 | ## 🕺   Contribute 295 | 296 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device 297 | 2. Install dependencies using Yarn: `yarn install` 298 | 3. Make the necessary changes and ensure that the tests are passing using `yarn test` 299 | 4. Send a pull request 🙌 300 | --------------------------------------------------------------------------------