Skip to content
This repository has been archived by the owner on Sep 10, 2022. It is now read-only.

Proposal: compose several nested render-props components into a single one #600

Open
gnapse opened this issue Jan 29, 2018 · 17 comments
Open

Comments

@gnapse
Copy link

gnapse commented Jan 29, 2018

This is motivated by this tweet by @acdlite and my shared need of such a feature, with my increasing tendency of heavily using render-props components.

Input: n render prop components
Output: 1 render prop component with n arguments

I attempted an initial version of this, which can be seen working here. Here's an excerpt of what it looks like:

return (
  <Composed
    components={[Title, Subtitle, Section]}
    render={(titleProps, subtitleProps, sectionProps) => (
      // ...
    )}
  />
);

The above code would be equivalent to this lengthier code:

return (
  <Title
    render={titleProps => (
      <Subtitle
        render={subtitleProps => (
          <Section
            render={sectionProps => (
              // ...
            )}
          />
        )}
      />
    )}
  />
);

However, this just scratches the surface, because it does not support at least of couple of things that I anticipate it'll need:

  1. Passing props to the individual render-props components that are being composed.
  2. Customizing how these render-props components receive the render prop (e.g. how the render prop is named on each component, whether they received via a render={() => ...} prop or a <Wrapper>{() => ...}</Wrapper>).

I submit here the proposal so we can discuss if and how to provide these features, and the syntax and form with which such a composer can be provided by this library. For instance, I developed it as a render prop itself, but it might also be desirable to have it as a HOC, since most of this library exposes HOCs.

@pokorson
Copy link
Contributor

@gnapse you may take a look on https://github.com/neoziro/recompact, it's doing something like transducer for components

@gnapse
Copy link
Author

gnapse commented Jan 30, 2018

@pokorson nice! Did not know about that library. Still, on a quick glance I did not see something providing what I describe above. Can you point directly to it if it's somewhere in there?

However, independently I found out about react-powerplug, which does provide something a-like, but I suspect it's intended to be used with the set of render props components that that library provides, and it's not suitable for general use. I'll check further.

Still I think this library should provide something like that on its own. I already use it extensively, and some other projects do, so jumping to recompact just for the flattening of the tree and for being a drop-in replacement of this, is not always feasable.

@pokorson
Copy link
Contributor

there was some discussion about this previously, you can check it here:
#182

and recompact states that it's fully compatible with recompose API so replacing it shouldn't be a problem.

@gnapse
Copy link
Author

gnapse commented Jan 31, 2018

Again, I can't see the relevance on insisting on recompact in this thread. It does not look like it has this feature I am proposing. Can we please stick to discussing the feature proposal?

@pokorson
Copy link
Contributor

pokorson commented Jan 31, 2018

@gnapse on discussion to feature proposal I've sent you a past issue. From what I saw the topic is coming back regularly and I haven't seen any plans to implement something like this. But I'm not a maintainer so we can wait what @istarkov or @wuct could say about this.

@pokorson
Copy link
Contributor

also could you post some example how would you expect it to look with hoc's instead of render props?

@gnapse
Copy link
Author

gnapse commented Jan 31, 2018

As I said, I don't see that past issue #182 as being the same as what I'm proposing here.

As a hoc it could look like this:

const CounterWithTimer = composed(Counter, <Timer interval={1000} />);

<CounterWithTimer>
  {(counterProps, timerProps) => ( ... )}
</CounterWithTimer>

The above being used as a render prop could look like this:

<Composed components={Counter, <Timer interval={1000} />}>
    {(counterProps, timerProps) => ( ... )}
</Composed>

@pokorson
Copy link
Contributor

pokorson commented Jan 31, 2018

I'm still referencing to this:

Input: n render prop components
Output: 1 render prop component with n arguments

The way it can work is explained in first comment.

And you're mixing these two concepts (hocs and render props). hoc is a function that return component. You should do composition on hoc's not components.

const withCounterAndTimer = composed(withCounter, withTimer);

const CounterTimerComponent = withCounterAndTimer(
  (counterProps, timerProps) => ( ... )
);

I think there's no way of passing props to your component from 2 different components except of nesting everything.

<Counter>
  {(counterProps) => (
    <Timer>
      {(timerProps) => (
        <YourComponent {...timerProps} {...counterProps} />
      )}
    </Timer>
  )}
</Counter>

@gnapse
Copy link
Author

gnapse commented Jan 31, 2018

I'm not sure what you mean is not possible to pass to my component without nesting. The point of the feature I am proposing is precisely to be able to do your second snippet of code above without having to nest things, and here's how it'd be possible.

<Composed components={[Counter, Timer]}>
  {(counterProps, timerProps) => (
    <YourComponent {...timerProps} {...counterProps} />
  )}
</Composed>

And this is indeed possible, as is evidenced in the code sandbox example I linked to above.

@pokorson
Copy link
Contributor

can you post more concrete example what's your use case for this?

@gnapse
Copy link
Author

gnapse commented Jan 31, 2018

Ok, at this point I think I may not be explaining my self correctly, but I'll try my best to attempt at explaining the need for this in more depth. The concept isn't at all new, and I was definitely not the one with the need in the first place. The tweet I mentioned at the beginning is one example of people needing this in various situations, now that render prop components are more and more popular and ubiquitous.

Also the library react-powerplug which I already linked to, includes a similar utility component to achieve this exact same thing, but more oriented to work with all the render prop components that that library exposes, and not so much in the general case.

Libraries like react-fns expose a lot of browser apis wrapped in a more declarative api based on render prop components. The powerplug one I mentioned above too. Imagine yourself leveraging stuff from several render prop components, all of them rendered because you need in the same place two or more of them. You'd have to nest them, and the excessive nesting quickly becomes unmanageable, often as early as three level deeps. A utility like the one I am proposing flattens that nesting of render prop components into a single one, which is sort of like composing them.

@pokorson
Copy link
Contributor

Ok, I think I got your point and I agree, composing with render props components is really ugly :P that's why I still prefer HOC's for most cases.
I think this library won't go anywhere close to render props pattern and I guess the only improvement that might happen for existing HOC's composition is the one from issue I posted - creating pipe of props transformations without component.

But let's wait what people smarter than me can say about this 😃

@gnapse
Copy link
Author

gnapse commented Jan 31, 2018

For the time being I plan to publish this as an independent library providing this feature alone. Thanks anyway for your feedback and your patience. I'll leave this issue open in case the discussion follows along.

@wuct
Copy link
Contributor

wuct commented Feb 11, 2018

Thanks for your proposal. As what @pokorson said, Recompose is about higher-order components, so it seems weird to have a utility component for render-props components.

@qm3ster
Copy link

qm3ster commented Mar 20, 2018

I'm so confused. Is recompact a strict superset of this library?
Does this library have a roadmap where it's going somewhere new?

Also, unrelated, but are the components in this library available as transformers to compose inside a mapPropsStream() by hand?

@evenchange4
Copy link
Contributor

Here is another proposal making render props component composable in recompose way :)

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>
)

@lukewlms
Copy link

FYI - this library looks most popular right now for composing render props into a single component:

https://github.com/pedronauck/react-adopt

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants