Skip to content

Commit

Permalink
Merge pull request acdlite#677 from evenchange4/feature/render-props-hoc
Browse files Browse the repository at this point in the history
feat: add fromRenderProps [BREAKING CHANGES]
  • Loading branch information
wuct authored Jun 21, 2018
2 parents 3f3ebf8 + fe82011 commit 38d637b
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 21 deletions.
18 changes: 9 additions & 9 deletions .size-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"lib/packages/recompose/dist/Recompose.umd.js": {
"bundled": 85035,
"minified": 31377,
"gzipped": 10006
"bundled": 85956,
"minified": 31664,
"gzipped": 10060
},
"lib/packages/recompose/dist/Recompose.min.js": {
"bundled": 81529,
"minified": 30125,
"gzipped": 9618
"bundled": 82334,
"minified": 30381,
"gzipped": 9668
},
"lib/packages/recompose/dist/Recompose.esm.js": {
"bundled": 31436,
"minified": 15712,
"gzipped": 3522,
"bundled": 32320,
"minified": 16089,
"gzipped": 3579,
"treeshaked": {
"rollup": 601,
"webpack": 1863
Expand Down
56 changes: 52 additions & 4 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ const PureComponent = pure(BaseComponent)
+ [`getContext()`](#getcontext)
+ [`lifecycle()`](#lifecycle)
+ [`toClass()`](#toclass)
+ [`withRenderProps()`](#withrenderprops)
+ [`toRenderProps()`](#torenderprops)
+ [`fromRenderProps()`](#fromrenderprops)
* [Static property helpers](#static-property-helpers)
+ [`setStatic()`](#setstatic)
+ [`setPropTypes()`](#setproptypes)
Expand Down Expand Up @@ -565,10 +566,10 @@ Takes a function component and wraps it in a class. This can be used as a fallba
If the base component is already a class, it returns the given component.
### `withRenderProps()`
### `toRenderProps()`
```js
withRenderProps(
toRenderProps(
hoc: HigherOrderComponent
): ReactFunctionalComponent
```
Expand All @@ -578,12 +579,59 @@ Creates a component that accepts a function as a children with the high-order co
Example:
```js
const enhance = withProps(({ foo }) => ({ fooPlusOne: foo + 1 }))
const Enhanced = withRenderProps(enhance)
const Enhanced = toRenderProps(enhance)

<Enhanced foo={1}>{({ fooPlusOne }) => <h1>{fooPlusOne}</h1>}</Enhanced>
// renders <h1>2</h1>
```
### `fromRenderProps()`
```js
fromRenderProps(
RenderPropsComponent: ReactClass | ReactFunctionalComponent,
propsMapper: (props: Object) => Object,
renderPropName?: string
): HigherOrderComponent
```
Takes a **render props** component and a function that maps props to a new collection of props that are passed to the base component.
The default value of third parameter (`renderPropName`) is `children`. You can use any prop (e.g., `render`) for render props component to work.
> Check the official documents [Render Props](https://reactjs.org/docs/render-props.html#using-props-other-than-render) for more details.
```js
import { fromRenderProps } from 'recompose';
const { Consumer: ThemeConsumer } = React.createContext({ theme: 'dark' });
const { Consumer: I18NConsumer } = React.createContext({ i18n: 'en' });
const RenderPropsComponent = ({ render, value }) => render({ value: 1 });

const EnhancedApp = compose(
// Context (Function as Child Components)
fromRenderProps(ThemeConsumer, ({ theme }) => ({ theme })),
fromRenderProps(I18NConsumer, ({ i18n }) => ({ locale: i18n })),
// Render props
fromRenderProps(RenderPropsComponent, ({ value }) => ({ value }), 'render'),
)(App);

// Same as
const EnhancedApp = () => (
<ThemeConsumer>
{({ theme }) => (
<I18NConsumer>
{({ i18n }) => (
<RenderPropsComponent
render={({ value }) => (
<App theme={theme} locale={i18n} value={value} />
)}
/>
)}
</I18NConsumer>
)}
</ThemeConsumer>
)
```
## Static property helpers
Expand Down
72 changes: 72 additions & 0 deletions src/packages/recompose/__tests__/fromRenderProps-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react'
import { mount } from 'enzyme'
import { fromRenderProps, compose, toRenderProps, defaultProps } from '../'

test('fromRenderProps passes additional props to base component', () => {
const RenderPropsComponent = ({ children }) => children({ i18n: 'zh-TW' })
const EnhancedComponent = fromRenderProps(
RenderPropsComponent,
({ i18n }) => ({
i18n,
})
)('div')
expect(EnhancedComponent.displayName).toBe('fromRenderProps(div)')

const div = mount(<EnhancedComponent />)
expect(div.html()).toBe(`<div i18n="zh-TW"></div>`)
})

test('fromRenderProps passes additional props to base component with custom renderPropName', () => {
const RenderPropsComponent = ({ render }) => render({ i18n: 'zh-TW' })
const EnhancedComponent = fromRenderProps(
RenderPropsComponent,
({ i18n }) => ({
i18n,
}),
'render'
)('div')
expect(EnhancedComponent.displayName).toBe('fromRenderProps(div)')

const div = mount(<EnhancedComponent />)
expect(div.html()).toBe(`<div i18n="zh-TW"></div>`)
})

test('fromRenderProps passes additional props to base component with 2 RenderPropsComponents', () => {
const RenderPropsComponent1 = ({ children }) => children({ theme: 'dark' })
const RenderPropsComponent2 = ({ render }) => render({ i18n: 'zh-TW' })
const EnhancedComponent = compose(
fromRenderProps(
RenderPropsComponent1,
({ theme }) => ({ theme }),
'children'
),
fromRenderProps(
RenderPropsComponent2,
({ i18n }) => ({ locale: i18n }),
'render'
)
)('div')
expect(EnhancedComponent.displayName).toBe(
'fromRenderProps(fromRenderProps(div))'
)

const div = mount(<EnhancedComponent />)
expect(div.html()).toBe(`<div theme="dark" locale="zh-TW"></div>`)
})

test('fromRenderProps meet toRenderProps', () => {
const RenderPropsComponent = toRenderProps(
defaultProps({ foo1: 'bar1', foo2: 'bar2' })
)

const EnhancedComponent = fromRenderProps(
RenderPropsComponent,
({ foo1 }) => ({
foo: foo1,
})
)('div')
expect(EnhancedComponent.displayName).toBe('fromRenderProps(div)')

const div = mount(<EnhancedComponent />)
expect(div.html()).toBe(`<div foo="bar1"></div>`)
})
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react'
import { mount } from 'enzyme'
import { withRenderProps, defaultProps } from '../'
import { toRenderProps, defaultProps } from '../'

test('withRenderProps creates a component from defaultProps HOC', () => {
test('toRenderProps creates a component from defaultProps HOC', () => {
const enhance = defaultProps({ foo: 'bar' })
const Enhanced = withRenderProps(enhance)
const Enhanced = toRenderProps(enhance)

expect(Enhanced.displayName).toBe('defaultProps(RenderPropsComponent)')

Expand Down
43 changes: 43 additions & 0 deletions src/packages/recompose/__tests__/types/test_fromRenderProps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* @flow */
import React from 'react'
import { compose, fromRenderProps } from '../..'

import type { HOC } from '../..'

const RenderPropsComponent1 = ({ children }) => children({ theme: 'dark' })
const RenderPropsComponent2 = ({ render }) => render({ i18n: 'zh-TW' })

type EnhancedCompProps = {||}

const Comp = ({ i18n, theme }) =>
<div>
{i18n}
{theme}
{
// $ExpectError
(i18n: number)
}
{
// $ExpectError
(theme: number)
}
</div>

const enhancer: HOC<*, EnhancedCompProps> = compose(
fromRenderProps(RenderPropsComponent1, props => ({
theme: props.theme,
// $ExpectError property not found
err: props.iMNotExists,
})),
fromRenderProps(
RenderPropsComponent2,
props => ({
i18n: props.i18n,
// $ExpectError property not found
err: props.iMNotExists,
}),
'render'
)
)

const EnhancedComponent = enhancer(Comp)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* @flow */

import * as React from 'react'
import { compose, withProps, withRenderProps, withHandlers } from '../..'
import { compose, withProps, toRenderProps, withHandlers } from '../..'
import type { HOC } from '../..'

const enhance: HOC<*, {| +x: number |}> = compose(
Expand All @@ -15,7 +15,7 @@ const enhance: HOC<*, {| +x: number |}> = compose(
})
)

const WithProps = withRenderProps(enhance)
const WithProps = toRenderProps(enhance)

const Comp = () =>
<WithProps x={1}>
Expand Down
28 changes: 28 additions & 0 deletions src/packages/recompose/fromRenderProps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import setDisplayName from './setDisplayName'
import wrapDisplayName from './wrapDisplayName'

const fromRenderProps = (
RenderPropsComponent,
propsMapper,
renderPropName = 'children'
) => BaseComponent => {
const baseFactory = React.createFactory(BaseComponent)
const renderPropsFactory = React.createFactory(RenderPropsComponent)

const FromRenderProps = ownerProps =>
renderPropsFactory({
[renderPropName]: props =>
baseFactory({ ...ownerProps, ...propsMapper(props) }),
})

if (process.env.NODE_ENV !== 'production') {
return setDisplayName(wrapDisplayName(BaseComponent, 'fromRenderProps'))(
FromRenderProps
)
}

return FromRenderProps
}

export default fromRenderProps
3 changes: 2 additions & 1 deletion src/packages/recompose/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export { default as withContext } from './withContext'
export { default as getContext } from './getContext'
export { default as lifecycle } from './lifecycle'
export { default as toClass } from './toClass'
export { default as withRenderProps } from './withRenderProps'
export { default as toRenderProps } from './toRenderProps'
export { default as fromRenderProps } from './fromRenderProps'

// Static property helpers
export { default as setStatic } from './setStatic'
Expand Down
10 changes: 9 additions & 1 deletion src/packages/recompose/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,17 @@ declare export function createEventHandler(): {
handler: Function,
}

declare export function withRenderProps<B, E>(
declare export function toRenderProps<B, E>(
hoc: HOC<B, E>
): React$ComponentType<{|
...$Exact<E>,
children: (b: B) => React$Node,
|}>

declare export function fromRenderProps<B, E, RenderPropName, Props>(
RenderPropsComponent: Component<{
[RenderPropName]: (Props) => React$Node,
}>,
propsMapper: ((props: Props) => B),
renderPropName?: RenderPropName
): HOC<{ ...$Exact<E>, ...B }, E>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function withRenderProps(hoc) {
export default function toRenderProps(hoc) {
const RenderPropsComponent = props => props.children(props)
return hoc(RenderPropsComponent)
}

0 comments on commit 38d637b

Please sign in to comment.