Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update props.components as functions to component({results, render}) #39

Merged
merged 7 commits into from
Mar 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

### v5.0.0 (2018/3/22)

**Breaking Changes**

* When a function is included in the `components` array, it will now be called
with a different signature. Previously, it was called with one argument, `results`,
an array of the currently-accumulated results.

In React Composer v5, the function will be called with an object with two properties,
`results` and `render`. `results` is the same value as before, and `render` is the
render prop that you should place on the [React element](https://reactjs.org/docs/glossary.html#elements)
that is returned by the function.

* `mapResult` and `renderPropName` have been removed. The new signature of the function
described above gives you the information that you need to map the results, or to use
a custom render prop name.

If you need help migrating from an earlier version of React Composer, we encourage you to
read the new examples in the README. They demonstrate how you can use the new
API to accomplish the things that you previously used `renderPropName` and `mapResult` for.

### v4.1.0 (2018/2/10)

**Improvements**
Expand Down
169 changes: 120 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

Compose render prop components.

### Motivation
## Motivation

Render props are great. Using a component with a render prop looks like the following:

Expand Down Expand Up @@ -68,76 +68,140 @@ or [yarn](https://yarnpkg.com/):
yarn add react-composer
```

### API
## API

This library has one export: `Composer`.
This library has one, default export: `Composer`.

#### `<Composer />`
### `<Composer />`

Compose multiple render prop components together. The props are as
follows:

##### `components`
### `props.children`

The render prop components to compose. This is an array of [React elements](https://reactjs.org/docs/glossary.html#elements) and/or functions that return elements given the currently accumulated results.
A render function that is called with an array of results accumulated from the render prop components.

```jsx
<Composer components={[]}>
{results => {
/* Do something with results... Return a valid React element. */
}}
</Composer>
```

### `props.components`

The render prop components to compose. This is an array of [React elements](https://reactjs.org/docs/glossary.html#elements) and/or render functions that are invoked with a render function and the currently accumulated results.

```jsx
<Composer
components={[
// Simple elements may be passed where previous results are not required.
// React elements may be passed for basic use cases
// props.children will be provided via React.cloneElement
<Outer />,
// A function may be passed that will be invoked with the currently accumulated results.
// Functions provided must return a valid React element.
([outerResults]) => <Middle results={[outerResults]} />,
([outerResults, middleResults]) => (
<Inner results={[outerResults, middleResults]} />

// Render functions may be passed for added flexibility and control
({ results, render }) => (
<Middle previousResults={results} children={render} />
)
]}>
{([outerResults, middleResults, innerResults]) => {
/* ... */
{([outerResult, middleResult]) => {
/* Do something with results... Return a valid React element. */
}}
</Composer>
```

> Note: You do not need to specify the render prop on the components. If you do specify the render prop, it will
> be ignored.
> **Note:** You do not need to provide `props.children` to the React element entries in `props.components`. If you do provide `props.children` to these elements, it will be ignored and overwritten.

##### `children`
#### `props.components` as render functions

A function that is called with an array of results from the render prop
components.
A render function may be passed instead of a React element for added flexibility.

##### `renderPropName`
Render functions provided must return a valid React element. Render functions will be invoked with an object containing 2 properties:

The name of the component's render prop. Defaults to `"children"`.
1. `results`: The currently accumulated results. You can use this for render prop components which depend on the results of other render prop components.
2. `render`: The render function for the component to invoke with the value produced. Plug this into your render prop component. This will typically be plugged in as `props.children` or `props.render`.

> Note: Components typically use `children` or `render` as the render prop. Some
> even accept both.
```jsx
<Composer
components={[
// props.components may contain both elements and render functions
<Outer />,
({ /* results, */ render }) => <SomeComponent children={render} />
]}>
{results => {
/* Do something with results... */
}}
</Composer>
```

##### `mapResult`
## Examples and Guides

A function that is called with the same arguments that each component's render
prop is called with. This can be used to change the result that each component passes
down.
### Example: Render prop component(s) depending on the result of other render prop component(s)

Typically, this is useful for a component that passes multiple arguments to its
render prop. You could, for instance, map the arguments to an array:
```jsx
<Composer
components={[
<Outer />,
({ results: [outerResult], render }) => (
<Middle fromOuter={outerResult} children={render} />
),
({ results, render }) => (
<Inner fromOuterAndMiddle={results} children={render} />
)
// ...
]}>
{([outerResult, middleResult, innerResult]) => {
/* Do something with results... */
}}
</Composer>
```

### Example: Render props named other than `props.children`.

By default, `<Composer />` will enhance your React elements with `props.children`.

Render prop components typically use `props.children` or `props.render` as their render prop. Some even accept both. For cases when your render prop component's render prop is not `props.children` you can plug `render` in directly yourself. Example:

```jsx
<Composer
components={[<RenderPropComponent />]}
mapResult={function() {
return Array.from(arguments);
}}>
{() => { ... }}
components={[
// Support varying named render props
<RenderAsChildren />,
({ render }) => <RenderAsChildren children={render} />,
({ render }) => <RenderAsRender render={render} />,
({ render }) => <CustomRenderPropName renderItem={render} />
// ...
]}>
{results => {
/* Do something with results... */
}}
</Composer>
```

> Note: This is an advanced feature that you won't often need to use, but it's here should you need it.
### Example: Render prop component(s) that produce multiple arguments

### Guides
Example of how to handle cases when a component passes multiple arguments to its render prop rather than a single argument.

```jsx
<Composer
components={[
<Outer />,
// Differing render prop signature (multi-arg producers)
({ render }) => (
<ProducesMultipleArgs>
{(one, two) => render([one, two])}
</ProducesMultipleArgs>
),
<Inner />
]}>
{([outerResult, [one, two], innerResult]) => {
/* Do something with results... */
}}
</Composer>
```

#### Limitations
### Limitations

This library only works for render prop components that have a single render
prop. So, for instance, this library will not work if your component has an API like the following:
Expand All @@ -146,7 +210,7 @@ prop. So, for instance, this library will not work if your component has an API
<RenderPropComponent onSuccess={onSuccess} onError={onError} />
```

#### Render Order
### Render Order

The first item in the `components` array will be the outermost component that is rendered. So, for instance,
if you pass
Expand All @@ -163,16 +227,16 @@ then your tree will render like so:
- C
```

#### Console Warnings
### Console Warnings

Render prop components often specify with [Prop Types](https://reactjs.org/docs/typechecking-with-proptypes.html)
that the render prop is required. When using these components with React Composer, you may get a warning to the
Render prop components often specify with [PropTypes](https://reactjs.org/docs/typechecking-with-proptypes.html)
that the render prop is required. When using these components with React Composer, you may get a warning in the
console.

Although this does not affect the functionality of React Composer, it may be annoying to you. One way
avoid the warnings is to define the render prop as an empty function.
One way to eliminate the warnings is to define the render prop as an empty function knowning that `Composer` will
overwrite it with the real render function.

```js
```jsx
<Composer
components={[
<RenderPropComponent {...props} children={() => null} />
Expand All @@ -181,11 +245,18 @@ avoid the warnings is to define the render prop as an empty function.
>
```

We understand that this boilerplate is not ideal. We have another proposed solution to this problem that you might like. Unfortunately,
the downside to this other solution is that it is a breaking API change. To read more about it, and to share your opinion on whether we
should make the breaking change, head over to [Issue #43](https://github.com/jamesplease/react-composer/issues/43).
Alternatively, you can leverage the flexibility of the `props.components` as functions API and plug the render function in directly yourself.

```jsx
<Composer
components={[
({render}) => <RenderPropComponent {...props} children={render} />
]}
// ...
>
```

#### Example Usage
### Example Usage

Here are some examples of render prop components that benefit from React Composer:

Expand All @@ -194,7 +265,7 @@ Here are some examples of render prop components that benefit from React Compose

Do you know of a component that you think benefits from React Composer? Open a Pull Request and add it to the list!

### Contributing
## Contributing

Are you interested in helping out with this project? That's awesome 鈥撀爐hank you! Head on over to
[the contributing guide](./CONTRIBUTING.md) to get started.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-composer",
"version": "4.1.0",
"version": "5.0.0",
"description": "Compose render prop components",
"main": "lib/index.js",
"module": "es/index.js",
Expand Down Expand Up @@ -67,4 +67,4 @@
"dependencies": {
"prop-types": "^15.6.0"
}
}
}
86 changes: 32 additions & 54 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,41 @@
import React from 'react';
import { cloneElement } from 'react';
import PropTypes from 'prop-types';

export default function Composer({
components,
children,
renderPropName,
mapResult
}) {
if (typeof children !== 'function') {
return null;
}

/**
* Recursively build up elements from props.components and accumulate `results` along the way.
* @param {Array.<ReactElement|Function>} components
* @param {Array} results
* @returns {ReactElement}
*/
function chainComponents(components, results) {
// Once components is exhausted, we can render out the results array.
if (!components[0]) {
return children(results);
}

return React.cloneElement(
// Each props.components entry is either an element or function [element factory]
// When it is a function, produce an element by invoking it with currently accumulated results.
typeof components[0] === 'function'
? components[0](results)
: components[0],
// Enhance the element's props with the render prop.
{
[renderPropName]() {
return chainComponents(
// Remove the current component and continue.
components.slice(1),
// results.concat([mapped]) ensures [...results, mapped] instead of [...results, ...mapped]
results.concat(
mapResult ? [mapResult.apply(null, arguments)] : arguments[0]
)
);
}
}
);
}

return chainComponents(components, []);
export default function Composer(props) {
return renderRecursive(props.children, props.components);
}

Composer.propTypes = {
children: PropTypes.func,
children: PropTypes.func.isRequired,
components: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.element, PropTypes.func])
),
renderPropName: PropTypes.string,
mapResult: PropTypes.func
).isRequired
};

Composer.defaultProps = {
components: [],
renderPropName: 'children'
};
/**
* Recursively build up elements from props.components and accumulate `results` along the way.
* @param {function} render
* @param {Array.<ReactElement|Function>} remaining
* @param {Array} [results]
* @returns {ReactElement}
*/
function renderRecursive(render, remaining, results) {
results = results || [];
// Once components is exhausted, we can render out the results array.
if (!remaining[0]) {
return render(results);
}

// Continue recursion for remaining items.
// results.concat([value]) ensures [...results, value] instead of [...results, ...value]
function nextRender(value) {
return renderRecursive(render, remaining.slice(1), results.concat([value]));
}

// Each props.components entry is either an element or function [element factory]
return typeof remaining[0] === 'function'
? // When it is a function, produce an element by invoking it with "render component values".
remaining[0]({ results, render: nextRender })
: // When it is an element, enhance the element's props with the render prop.
cloneElement(remaining[0], { children: nextRender });
}
Loading