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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cascading props #5943

Closed
esseswann opened this issue Jan 29, 2016 · 14 comments
Closed

Cascading props #5943

esseswann opened this issue Jan 29, 2016 · 14 comments

Comments

@esseswann
Copy link

After using react for a while I came up with a strange idea to support some kind of cascading props.
So we have parent component

const Parent = React.createClass({
  render () {
    return (
      <div>
        <Child />
        <Child />
        <Child text='some text'/>
      </div>
    )
  }
})

and then a child component

const Child = React.createClass({
  render () {
    return (
      <div>{this.props.text}</div>
    )
  }
})

and then we call Parent component with following prop
<Parent Child={{text: 'cascading prop'}}/>
It would render as this

cascading prop
cascading prop
some text (or maybe override?)

Going further I would propse the use of CSS-like traversal selectors on these props (i.e. >, :first-child). I know all of it looks very much like some kind of mad antipattern, but I would like to hear why, besides obvious performance isssues.

The main reason behind it is that a lot of times you have to change a little bit insides of a component in one particular place and have to implement new methods\props and then declare them in parent component anyways.

@blainekasten
Copy link
Contributor

Sounds like context :)

@esseswann
Copy link
Author

Don't I have to explicitly use context variables like this.context.something?

@conorhastings
Copy link

@esseswann
Copy link
Author

It's not the same. What I'm proposing is to make a special kind of prop which traverses down and sets all matched component props like CSS selectors do

@texttechne
Copy link

Ok, the use case you provided in detail is covered by default props. But you're after something bigger: You want to control child components and their props from grandparent components. This is your big picture, right?

I know all of it looks very much like some kind of mad antipattern, but I would like to hear why, besides obvious performance isssues.

I think what you propose is an antipattern. So let's take your example:
<Parent Child={{text: 'cascading prop'}}/> (let's assume that you make this call from your <Grandparent> component)

What you are doing here requires an implicit knowledge of how the <Parent> component works:

  1. That it is structured via <Child> components
  2. That a <Child> component has a text property (of type string)

This kind of knowledge - that, where, when, and how the <Child> component is used - should be encapsulated within the <Parent> component, because it is an implementation detail of the <Parent> component and implementation details are subject to change. E.g. if you want to refactor the <Parent> to use <Nephew> instead of <Child> you get into trouble really fast.

You don't have these kind of problems with React as it is. Let's say your implementation looks like this: <Parent childText="simpleText" />. Now you don't have any problem at all with refactoring the <Parent>: You can switch from <Child> to <Nephew> without any further modifications to any other component.

The crucial point is that the <Grandparent> component should only know about the interface (props in the case of React) of the <Parent> and the <Parent> in turn should only know about the interface of the <Child>. And as long as the interface (again, the props) won't change you are free to change the implementation of your component. What you're after is convenience while you should strive for simplicity.

The main reason behind it is that a lot of times you have to change a little bit insides of a component in one particular place and have to implement new methods\props and then declare them in parent component anyways.

Your API design, i.e. the interface, of your components appears to be brittle. Then again, it is really hard to get this "right"...

@esseswann
Copy link
Author

@texttechne Thnx for a thoughtful answer

What you are doing here requires an implicit knowledge of how the Parent component works:

Actually I kind of don't want to know how it works, I just want to set some props up if they occur, likewise I don't know when I make a selector in CSS.

Good example is https://github.com/callemall/material-ui (which is great), where they have to expose some guts of their components but part of them are not exposed because they use context for global theme settings. Which is kind of wacky, even though it seems reliable.

You can switch from Child to Nephew without any further modifications to any other component.

Yes, but from overall usability point of view I will likely change how grandparent calls parent and how parent handles props anyway, because Nephew props will probably differ. And it bothers me because it makes my components seem less isolated from the context they're used at especially if I tend to make them more reusable and universal, like many UI parts are.

@texttechne
Copy link

Yes, but from overall usability point of view I will likely change how grandparent calls parent and how parent handles props anyway, because Nephew props will probably differ.

As I said, interface / API design is the hard part, the open challenge. In general changing the implementation shouldn't affect the interface, so you should think really hard how you design it. Furthermore, when you think this occurs quite often, challenge your assumptions about your component: Why did you need to change your interface? Have you really isolated a single component for one purpose or does it serve more than one purpose? How would the ideal API look like? ...

And yes, changing the interface always hurts. But this challenge is up to you when it comes to component design, not up to the framework you use. This challenge is recurring for any API, be it a programming language, WebService or framework. Imagine you want to change the implementation of a specific language feature, but you cannot make the needed API change for backwards compatibility reasons...

This revolves all around interface / API design, which isn't specific to React. So I think you should close this issue.

@esseswann
Copy link
Author

I would completely agree with you if not for the fact that if people would be so good in designing their apps from scratch we would probably still use only JS+HTML with heavy OOP.

Such a structure would be a better example

Grandparent
--Parent
---Child
---Child
--Uncle
---Child
--Auntie
---Child
---Child

It's problematic to access Child properties from Grandparent, even though such a scenario is quite reccuring, especially considering heavy folding which is popular. Actually context would do the trick, if any children prop had access to it by default (and some cascading magic for more than one context in mid-children). And it even would've been a FP way.
The biggest problem is that Child component might be universal, suitable not only for current Grandparent usecase and for example coming from ever-updating library. Hence you'de have to update everything even if a small change in Child component happened.
So my question still remains, I don't want to hear that I can live without it, I want to hear why it might be dangerous and leading to bad practices.

@jimfb
Copy link
Contributor

jimfb commented Feb 1, 2016

I agree with @texttechne:

What you are doing here requires an implicit knowledge of how the component works

In general, a component should be a black box. As soon as you start peaking into the component implementation, things get really messy.

@esseswann said:

Actually I kind of don't want to know how it works, I just want to set some props up if they occur, likewise I don't know when I make a selector in CSS.

I understand what you're going for there, but (IMHO) this is one of the worst properties of CSS (lack of style isolation). At the very least, it's a double edged sword.

Imagine that you are designing a page, and you like big checkboxes, so you double the size of all the checkboxes on the page using a css selector. But a component that used a checkbox internally was depending on the size of the checkbox to align with the size of another node. Now things are broken. You can no longer import any arbitrary components, because the components now depend on the ambient styles of the page. This is the danger / bad practice.

It is an interesting discussion. Thanks for proposing it! But I'm going to close this out for the reasons @texttechne mentioned. Feel free to continue the discussion on this thread. FWIW, I think you could use context to achieve the behavior your looking for.

@jimfb jimfb closed this as completed Feb 1, 2016
@esseswann
Copy link
Author

@jimfb TY for a response, you gave some good explanation, but I still have some concerns

In general, a component should be a black box. As soon as you start peaking into the component implementation, things get really messy.

I feel a bit dumb but I don't understand how controlling props of a component from grandparent differs from controlling the same props from parent if in a corresponding use case I will have to expose those props through parent or through context anyways.

But a component that used a checkbox internally was depending on the size of the checkbox to align with the size of another node.

So there are two ways: make it dependant on something more abstract or add logic to a component which would make it less reusable.
The thing is that unlike CSS which can affect almost everything, cascading props should affect, obviously only props that component is made to recieve without breaking.

@jimfb
Copy link
Contributor

jimfb commented Feb 1, 2016

@esseswann A parent can reason about about all the possible props that it chooses to forward to its child, but it can't easily reason about all the things a grandparent might do if the grandparent is allowed to bypass the parent's API.

@esseswann
Copy link
Author

As far as I checked Parent context overrides GrandParent context why not the same for props?
(BTW @webpack HMR somehow ignores deletion of contextTypes from Parent and renders this.context.something as undefinied only after F5)

Overall subject referenced as Inversion of Control here

@bergkvist
Copy link

bergkvist commented Apr 29, 2020

A potential use case for this would be

Cascading onContextMenu prop

Usually, a custom context menu is something you want to be global on your site - but you want children to be able to overwrite the context menu to create a more specialized one.

This can't really be solved using the context api, since you have to manually hook into the context API from within the children. If you don't have direct control of the children, that isn't really an option.

Component libraries often won't expose the props of their internal DOM elements, which means passing down props manually in all the components you control isn't really an option.

Using a wrapper element

Of course, what you could do here is simply to wrap your entire application in a div, and then put the event listener on this div. Taking advantage of how bubbling events and stopPropagation works to make more specialized context menus.

The problem with this is that it makes the DOM tree more "noisy". It adds tags that are not strictly necessary.

@esseswann
Copy link
Author

esseswann commented Apr 30, 2020

@bergkvist
4 years of working with React since I created this issue and now I am more cautious about such requests :)
But if you want a thought experiment on this concept - cloning the elements deep inside the tree by some code-walker which replaces the props might achieve the desired results

{
  "type":"div",
  "key":null,
  "ref":null,
  "props":{
    // Walk down the tree to a desired element
    "children":{ // Clone the element with React.cloneElement()
      "type":"div",
      "key":null,
      "ref":null,
      "props":{
        "prop":"test" // Change this prop in the vdom tree
      },
      "_owner":null,
      "_store":{
      }
    }
  }, // Commit the modified element back to the vdom
  "_owner":null,
  "_store":{
  }
}

This is practically monkey patching

@jimfb Am I correct with this conceptually?

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

No branches or pull requests

6 participants