DEV Community

Discussion on: Passing Data from Child to Parent with React Hooks

Collapse
 
peerreynders profile image
peerreynders • Edited

From Wrong context warning? - 'Warning: owner-based and parent-based contexts differ' #3451

Is a component A a child of component B only if it A is passed as the children property to B?

AFAIK, yes.

According to Documentation inconsistencies around Parent/Child vs Owner/Ownee #7794

I think the biggest issue here is that owner/ownee almost never matters, so it is awkward to mention in the introductory text.

This is a curious statement given how often parent/child is being used in introductory tutorials to describe the relationship between a component and the components being used in its render method - most often attempting to state something like:

"Passing props is how information flows in React apps, from owners to ownees."

So it would seem that the official documentation simply tries to avoid the parent/child terminology unless it's referring to a relationship within the render result (i.e. the DOM tree or some intermediate representation thereof).

React.createElement

React.createElement(
  type,
  [props],
  [...children]
)
Enter fullscreen mode Exit fullscreen mode

So here it should be clear that type is the parent "node" to children - but notice that type (the parent) doesn't create children.

However community contributions to the documentation may re-introduce the notion of the "creational parent":

"Passing props is how information flows in React apps, from parents to children."

In the absence of definitive terminology (i.e. avoiding owner/ownee) it's easy to think of the component that creates another component (while rendering) as the "parent".

The "component tree" could be seen as a hierarchy that represents the rendering process but that is different from the result of the rendering process - the "tree of rendered elements".

Component render vs Document tree

It is possible to imagine the "tree of rendered elements" and draw boxes around the fragments that are rendered by specific components - at that point a component could be perceived as "containing" elements or nested components.

It could also be argued that the "parent-child relationship between nodes" isn't all that interesting in React due to the limitations of the children prop. While children can be used to pass slotted content to a parent component, children needs to be a "children as a function" for the more interesting cases. But in those cases best practice suggests to use render props/component injection - which lessens the importance of children (and by extension the parent-child relationship).

import { createElement as h, render, Component } from 'https://unpkg.com/preact?module';

// 1. In order to emulate a 'scoped slot'
//    the `children` prop has to be a
//    render prop so that the `MyList`
//    component can pass props to its `children`.
//
function MyList({ title, items, children }) {
  return (
    h('section', {class: 'my-list'},
      h('h1', null, title),
      h('ul', null,
        ...items.map(item =>
          h('li', {class: 'my-list__item'},
            children(item) // (1.)
          ))
      )
    )
  );
}

// Given that the slot content
// needs to receive props
// the content needs to be
// implemented as a component
// (`Shape` and `Color`)

function Shape({ shape }) {
  return (
    h('div', null,
      `${shape.name} `,
      h('small', null, `(${shape.sides} sides)`)
    )
  );
}

function Color({ color }){
  return (
    h('div', null,
      h('div', {class: 'swatch', style: `background: ${color.hex}`}),
      color.name
    )
  );
}

// 2. Here `MyList` is parent to `Shape`
//    but as `MyList` needs pass props to `Shape`
//    `children` to `MyList` needs to be function.
// 3. Similarly the `Color``children` to `MyList`
//    has to be a function.
//
function App({ data }) {
  return [
    h(MyList, {title: 'Shapes', items: data.shapes },
      shape => h(Shape, { shape }) // (2.)
    ),
    h(MyList, {title: 'Colors', items: data.colors },
      color => h(Color, { color }) // (3.)
    ),
  ];
}

const data = {
  shapes: [
    { name: 'Square', sides: 4 },
    { name: 'Hexagon', sides: 6 },
    { name: 'Triangle', sides: 3 },
  ],
  colors: [
    { name: 'Yellow', hex: '#F4D03F' },
    { name: 'Green', hex: '#229954' },
    { name: 'Purple', hex: '#9B59B6' },
  ],
};

render(
  h(App, { data }),
  document.getElementById('app')
);
Enter fullscreen mode Exit fullscreen mode

Full gist

Note that in the above example MyList is the parent of Shape (and Color) within the App component. But both Shape and Color have to be passed as "children as a function" (rather than simple child components) so that MyList can pass props to them. However typically this would be implemented with component injection - so the injected component is a simple prop rather than a child.

// 1. Here the 'scoped slot' is emulated with component injection:
//    - the `render` prop references the component to use
//    - the `name` prop identifies the prop name the
//      item should be passed by.
//
function MyList({ title, items, name, render: View }) {
  return (
    h('section', {class: 'my-list'},
      h('h1', null, title),
      h('ul', null,
        ...items.map(item =>
          h('li', {class: 'my-list__item'},
            h(View, { [name]: item }) // (1.)
          ))
      )
    )
  );
}

// ...

// The `App` component using component injection instead
//
// 2. The `Shape` component is injected for rendering
//    while the `item` should be passed via the `shape` prop.
// 3. The `Color` component is injected for rendering
//    while the `item` should be passed via the `color` prop.
//
// In effect the "slot content" is no longer passed as "children"
//
function App({ data }) {
  return [
    h(MyList, {title: 'Shapes', items: data.shapes, name: 'shape', render: Shape}), // (2.)
    h(MyList, {title: 'Colors', items: data.colors, name: 'color', render: Color})  // (3.)
  ];
}
Enter fullscreen mode Exit fullscreen mode

A "parent" is simply an element (or component) with an opening and closing tag (in JSX) and any elements (or components) between those tags are the "children" to that parent. The only time that relationship is really of any interest is with components with a slot:

  • the component with the slot is the "parent"
  • the content placed into the slot is the "child" (or children).

Using parent/child to also refer to the creational relationships between components can make discussions ambiguous and downright difficult when it is necessary to simultaneously discuss relationships within the creational (i.e. rendering) process and within the content model (e.g. DOM tree and the like).


Edit:

Another way to look at it - given:

function App() {
  const colorA = 'green';
  const colorB = 'yellow';
  const colorC = 'blue';
  const colorD = 'orange';

  return (
    <div>
      <CompA color={colorA}>
        <CompC color={colorC} />
      </CompA>
      <CompB color={colorB}>
        <CompD color={colorD} />
      </CompB>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • CompA is the parent to CompC
  • CompB is the parent to CompD
  • The props are being passed from App to CompA, CompB, CompC and CompD.

How does the statement:

"Passing props is how information flows in React apps, from parents to children."

hold up now?

  • No props are being passed from CompA to CompC.
  • No props are being passed from CompB to CompD.
  • All props come from App.

I guess:

  • CompA and CompB are parents from the content/document perspective.
  • App is a parent from the rendering perspective.

But nobody seems to bother with that distinction.