Quick summary ↬

In this article we discuss and learn about the use of repetition on React children and the ways to do it. We will focus on one of the useful methods, React.Children.toArray, which React gives us, which helps to repeat about the children in a way that ensures performance and determinism.

The most obvious and common object that developers in React work with is the children brace. In most cases, you do not have to understand how children stut lyk. But in some cases we want the children suggests that each child be wrapped in a different element / component, or to rearrange or cut it. In these cases, inspect how the children prop seems to be becoming essential.

In this article we look at a React utility React.Children.toArray with which we children record for inspection and repetition, some of the shortcomings and how to overcome them – through a small open-source package, to maintain our React code function as it is deterministically supposed to act, and performance maintained To keep. If you know the basics of React and at least have an idea of ​​what children plug in React, this article is for you.

While working with React, we mostly do not touch the children more than using it directly in React components.

function Parent({ children }) {
  return <div className="mt-10">{children}</div>;
}

But sometimes we have to repeat about the children plug so that we can improve or change the children without the user of the components doing so explicitly himself. One common use is to transfer the iteration index-related information to the parent components of a parent, as follows:

import { Children, cloneElement } from "react";

function Breadcrumbs({ children }) {
  const arrayChildren = Children.toArray(children);

  return (
    <ul
      style={{
        listStyle: "none",
        display: "flex",
      }}
    >
      {Children.map(arrayChildren, (child, index) => {
        const isLast = index === arrayChildren.length - 1;

        if (! isLast && ! child.props.link ) {
          throw new Error(
            `BreadcrumbItem child no. ${index + 1}
            should be passed a 'link' prop`
          )
        } 

        return (
          <>
            {child.props.link ? (
              <a
                href={child.props.link}
                style={{
                  display: "inline-block",
                  textDecoration: "none",
                }}
              >
                <div style={{ marginRight: "5px" }}>
                  {cloneElement(child, {
                    isLast,
                  })}
                </div>
              </a>
            ) : (
              <div style={{ marginRight: "5px" }}>
                {cloneElement(child, {
                  isLast,
                })}
              </div>
            )}
            {!isLast && (
              <div style={{ marginRight: "5px" }}>
                >
              </div>
            )}
          </>
        );
      })}
    </ul>
  );
}

function BreadcrumbItem({ isLast, children }) {
  return (
    <li
      style={{
        color: isLast ? "black" : "blue",
      }}
    >
      {children}
    </li>
  );
}

export default function App() {
  return (
    <Breadcrumbs>
      <BreadcrumbItem
        link="https://goibibo.com/"
      >
        Goibibo
      </BreadcrumbItem>

      <BreadcrumbItem
        link="https://goibibo.com/hotels/"
      >
        Hotels
      </BreadcrumbItem>

      <BreadcrumbItem>
       A Fancy Hotel Name
      </BreadcrumbItem>
    </Breadcrumbs>
  );
}


Here we do the following:

  1. We use the React.Children.toArray method to ensure that the children plug is always an array. If we do not do it, do children.length can blow because the children plug can be an object, an array or even a function. If we also try to use the array .map method on children directly it can inflate.
  2. In the parent Breadcrumbs component that we repeat about its children using the utility method React.Children.map.
  3. Because we have access to index within the iterator function (second argument of callback function of React.Children.map) we can determine whether the child is the last child or not.
  4. If this is the last child, we clone the element and give the isLast support it so that the child can style himself on it.
  5. If this is not the last child, we ensure that all children who are not the last child, a link support them by making a mistake if they do not. We clone the element as in step 4. and pass the isLast plug as we did before, but we also turn this cloned element into an anchor plate as well.

The user of Breadcrumbs and BreadcrumbItem no need to worry about which kids should have links and how they should be designed. Within the Breadcrumbs component, it is handled automatically.

This pattern of implicit bring in and / or have props state in the parent and the transfer of the state and state exchangers to the children as props becomes the composite component pattern. You may know this pattern from React Router Switch component, which takes Route components as its children:

// example from react router docs
// https://reactrouter.com/web/api/Switch

import { Route, Switch } from "react-router";

let routes = (
  <Switch>
    <Route exact path="https://smashingmagazine.com/">
      <Home />
    </Route>
    <Route path="/about">
      <About />
    </Route>
    <Route path="/:user">
      <User />
    </Route>
    <Route>
      <NoMatch />
    </Route>
  </Switch>
);

Now that we have determined that there are needs that we need to repeat children sometimes, and after using two of the children’s utilities React.Children.map and React.Children.toArray, let’s refresh our memory of one of them: React.Children.toArray.

More to jump! Read more below ↓

React.Children.toArray

Let’s start with an example of what this method does and where it can be useful.

import { Children } from 'react'

function Debugger({children}) {
  // let’s log some things
  console.log(children);
  console.log(
    Children.toArray(children)
  )
  return children;
}

const fruits = [
  {name: "apple", id: 1},
  {name: "orange", id: 2},
  {name: "mango", id: 3}
]

export default function App() {
  return (
    <Debugger>
        <a
          href="https://css-tricks.com/"
          style={{padding: '0 10px'}}
        >
          CSS Tricks
        </a>

        <a
          href="https://smashingmagazine.com/"
          style={{padding: '0 10px'}}
        >
          Smashing Magazine
        </a>

        {
          fruits.map(fruit => {
            return (
              <div key={fruit.id} style={{margin: '10px'}}>
                {fruit.name}
              </div>
            )
          })
        }
    </Debugger>
  )
}


We have a Debugger component, which has nothing to do with version – it just comes back children just like that. But it does record two values: children and React.Children.toArray(children).

When you open the console, you can see the difference.

  • The first statement to record children plug, shows the following as the data structure of its value:
[
  Object1, ----> first anchor tag
  Object2, ----> second anchor tag
  [
    Object3, ----> first fruit
    Object4, ----> second fruit
    Object5] ----> third fruit
  ]
]
  • The second statement that notes React.Children.toArray(children) logs:
[
  Object1, ----> first anchor tag
  Object2, ----> second anchor tag
  Object3, ----> first fruit
  Object4, ----> second fruit
  Object5, ----> third fruit
]

Let’s read the documentation of the method in React documents to make sense of what is happening.

React.Children.toArray give the children opaque data structure as a flat array with keys assigned to each child. Useful if you want to manipulate children’s collections in your rendering methods, especially if you want to rearrange or cut children before it is transmitted.

Let’s break it down:

  1. Show the children opaque data structure as a flat array.
  2. With keys assigned to each child.

The first point says that it children (which is an opaque data structure, meaning that it can be an object, array, or function, as described earlier) is converted to a flat array. Just as we saw in the example above. Furthermore, this GitHub Note also explain its behavior:

This (React.Children.toArray) do not pull children out of elements and flatten them, it would not really make sense. It flattens nested arrays and objects, i.e. so [['a', 'b'],['c', ['d']]] becomes something similar to ['a', 'b', 'c', 'd'].

React.Children.toArray(
  [
    ["a", "b"],
    ["c", ["d"]]
  ]
).length === 4;

Let’s see what the second point (‘With keys assigned to each child’) says, by extending one child each from the previous logs of the example.

Extended child of console.log(children)

{
  $$typeof: Symbol(react.element),
  key: null,
  props: {
    href: "https://smashingmagazine.com",
    children: "Smashing Magazine",
    style: {padding: "0 10px"}
  },
  ref: null,
  type: "a",
  // … other properties
}

Extended child of console.log(React.Children.toArray(children))

{
  $$typeof: Symbol(react.element),
  key: ".0",
  props: {
    href: "https://smashingmagazine.com",
    children: "Smashing Magazine",
    style: {padding: "0 10px"}
  },
  ref: null,
  type: "a",
  // … other properties
}

As you can see, except for the children prop in a flat arrangement, it also adds unique keys to each of its children. From the React documents:

React.Children.toArray() change keys to maintain the semantics of nested arrays when posting lists of children. In other words, toArray prefixes for each key in the returned array so that the key of each element to the input sequence it contains is reached.

Because the .toArray method can determine the order and location of children, it should make sure that it contains unique keys for each reconciliation and optimization.

Let’s pay a little more attention so that each element’s key is scoped to the input array containing it.by looking at the keys of each element of the second array (corresponding to console.log(React.Children.toArray(children))).

import { Children } from 'react'

function Debugger({children}) {
  // let’s log some things
  console.log(children);
  console.log(
    Children.map(Children.toArray(children), child => {
      return child.key
    }).join('n')
  )
  return children;
}

const fruits = [
  {name: "apple", id: 1},
  {name: "orange", id: 2},
  {name: "mango", id: 3}
]

export default function App() {
  return (
    <Debugger>
        <a
          href="https://css-tricks.com/"
          style={{padding: '0 10px'}}
        >
          CSS Tricks
        </a>
        <a
          href="https://smashingmagazine.com/"
          style={{padding: '0 10px'}}
        >
          Smashing Magazine
        </a>
        {
          fruits.map(fruit => {
            return (
              <div key={fruit.id} style={{margin: '10px'}}>
                {fruit.name}
              </div>
            )
          })
        }
    </Debugger>
  )
}
.0  ----> first link
.1  ----> second link
.2:0 ----> first fruit
.2:1 ----> second fruit
.2:2 ----> third fruit

As you can see, the fruit, which was originally a nested series in the original children array, has prefixed keys .2. The .2 correspond to the fact that they were part of a settlement. The suffix, namely :0 ,:1, :2 corresponds to the standard keys of the React elements (fruit). By default, React uses the index as the key, if no key is specified for the elements of a list.

So suppose you had three levels of nest inside children array, as follows:

import { Children } from 'react'

function Debugger({children}) {
  const retVal = Children.toArray(children)
  console.log(
    Children.map(retVal, child => {
      return child.key
    }).join('n')
  )
  return retVal
}

export default function App() {
  const arrayOfReactElements = [
    <div key="1">First</div>,
    [
      <div key="2">Second</div>,
      [
        <div key="3">Third</div>
      ]
    ]
  ];
  return (
    <Debugger>
      {arrayOfReactElements}
    </Debugger>
  )
}

The keys will look

.$1
.1:$2
.1:1:$3


The $1, $2, $3 suffixes are due to the original keys placed on the React elements in an array, otherwise React complains about a lack of keys 😉.

From what we have read so far, we can come to two use cases React.Children.toArray.

  1. If it is an absolute need children must always be an array, which you can use React.Children.toArray(children) instead. It will work perfectly even when children is also an object or a function.

  2. If you need to sort, filter or cut children prop you can rely on React.Children.toArray to always keep the unique keys of all the children.

There is a problem with React.Children.toArray 🤔. Let’s look at this piece of code to understand what the problem is:

import { Children } from 'react'

function List({children}) {
  return (
    <ul>
      {
        Children.toArray(
          children
        ).map((child, index) => {
          return (
            <li
              key={child.key}
            >
              {child}
            </li>
          )
        })
      }
    </ul>
  )
}

export default function App() {
  return (
    <List>
      <a
        href="https://css-tricks.com"
        style={{padding: '0 10px'}}
      >
        Google
      </a>
      <>
        <a
          href="https://smashingmagazine.com"
          style={{padding: '0 10px'}}
        >
          Smashing Magazine
        </a>
        <a
          href="https://arihantverma.com"
          style={{padding: '0 10px'}}
        >
          {"Arihant’s Website"}
        </a>
      </>
    </List>
  )
}


If you see what is shown to the children of the snippet, you will see that both the links are displayed within one li tag! 😱

This is because React.Children.toArray does not walk in fragments. So, what can we do about it? Luckily nothing 😅. We already have an open package called react-keyed-flatten-children. It’s a small function that performs its magic.

Let’s see what it does. In pseudocode (these points are linked to the actual code below), it does this:

  1. This is a feature that takes children as his only essential argument.
  2. Iterate past React.Children.toArray(children) and collect children in an accumulator array.
  3. While repeating, if a child node is a string or a number, it pushes the value as in the accumulator range.
  4. If the child node is a valid React element, clone it, give it the appropriate key, and push it to the battery pack.
  5. If the child node is a fragment, the function calls itself with fragments’ children as an argument (this is how it is walk through a fragment) and press the result to call itself in the accumulator range.
  6. While doing all this, it keeps track of the depth of intersection (of fragments) so that the children inside fragments have the right keys, in the same way that keys work with nested arrays, as we saw earlier above.
import {
  Children,
  isValidElement,
  cloneElement
} from "react";

import { isFragment } from "react-is";

import type {
  ReactNode,
  ReactChild,
} from 'react'

/*************** 1. ***************/
export default function flattenChildren(
  // only needed argument
  children: ReactNode,
  // only used for debugging
  depth: number = 0,
  // is not required, start with default = []
  keys: (string | number)[] = [] 
): ReactChild[] {
  /*************** 2. ***************/
  return Children.toArray(children).reduce(
    (acc: ReactChild[], node, nodeIndex) => {
      if (isFragment(node)) {
        /*************** 5. ***************/
        acc.push.apply(
          acc,
          flattenChildren(
            node.props.children,
            depth + 1,
            /*************** 6. ***************/
            keys.concat(node.key || nodeIndex)
          )
        );
      } else {
        /*************** 4. ***************/
        if (isValidElement(node)) {
          acc.push(
            cloneElement(node, {
              /*************** 6. ***************/
              key: keys.concat(String(node.key)).join('.')
            })
          );
        } else if (
          /*************** 3. ***************/
          typeof node === "string"
          || typeof node === "number"
        ) {
          acc.push(node);
        }
      }
      return acc; 
    },
    /*************** Acculumator Array ***************/
    []
  );
}

Let’s try our previous example again to use this feature and see for ourselves that it solves our problem.

import flattenChildren from 'react-keyed-flatten-children'
import { Fragment } from 'react'

function List({children}) {
  return (
    <ul>
      {
        flattenChildren(
          children
        ).map((child, index) => {
          return <li key={child.key}>{child}</li>
        })
      }
    </ul>
  )
}
export default function App() {
  return (
    <List>
      <a
        href="https://css-tricks.com"
        style={{padding: '0 10px'}}
      >
        Google
      </a>
      <Fragment>
        <a
          href="https://smashingmagazine.com"
          style={{padding: '0 10px'}}>
          Smashing Magazine
        </a>
        
        <a
          href="https://arihantverma.com"
          style={{padding: '0 10px'}}
        >
          {"Arihant’s Website"}
        </a>
      </Fragment>
    </List>
  )
}


Woooheeee! It works.

As an add-on, you may be interested in the latest testing-as I am currently writing this- 7 tests written for this utility. It would be nice to read the tests to deduce the function of the function.

The long-term problem with Children Utilities

React.Children is a leaky abstraction and is in maintenance mode. “

Dan Abramov

The problem with usage Children methods to change children behavior is that it only works for one level of components’ nest. If we wrap one of ours children in another component we lose the compatibility. Let’s see what I mean by that, by taking the first example we saw – the breadcrumbs.

import { Children, cloneElement } from "react";

function Breadcrumbs({ children }) {
  return (
    <ul
      style={{
        listStyle: "none",
        display: "flex",
      }}
    >
      {Children.map(children, (child, index) => {
        const isLast = index === children.length - 1;
        // if (! isLast && ! child.props.link ) {
        //   throw new Error(`
        //     BreadcrumbItem child no.
        //     ${index + 1} should be passed a 'link' prop`
        //   )
        // } 
        return (
          <>
            {child.props.link ? (
              <a
                href={child.props.link}
                style={{
                  display: "inline-block",
                  textDecoration: "none",
                }}
              >
                <div style={{ marginRight: "5px" }}>
                  {cloneElement(child, {
                    isLast,
                  })}
                </div>
              </a>
            ) : (
              <div style={{ marginRight: "5px" }}>
                {cloneElement(child, {
                  isLast,
                })}
              </div>
            )}
            {!isLast && (
              <div style={{ marginRight: "5px" }}>></div>
            )}
          </>
        );
      })}
    </ul>
  );
}

function BreadcrumbItem({ isLast, children }) {
  return (
    <li
      style={{
        color: isLast ? "black" : "blue",
      }}
    >
      {children}
    </li>
  );

}
const BreadcrumbItemCreator = () =>
  <BreadcrumbItem
    link="https://smashingmagazine.com"
  >
    Smashing Magazine
  </BreadcrumbItem>

export default function App() {
  return (
    <Breadcrumbs>
      <BreadcrumbItem
        link="https://goibibo.com/"
      >
        Goibibo
      </BreadcrumbItem>

      <BreadcrumbItem
        link="https://goibibo.com/hotels/"
      >
        Goibibo Hotels
      </BreadcrumbItem>

      <BreadcrumbItemCreator />

      <BreadcrumbItem>
        A Fancy Hotel Name
      </BreadcrumbItem>
    </Breadcrumbs>
  );
}


Although our new component <BreadcrumbItemCreator /> delivered, we Breadcrumb component has no way of link as a result, it is not displayed as a link.

To resolve this issue, the React team received an experimental API, which has now been discontinued respond-call-return.

Ryan Florence’s video explain this problem in detail, and how react-call-return fixed it. Since the package was never published in any version of React, there are plans to draw inspiration from it and get something ready for production.

Closure

In conclusion, we learned about:

  1. The React.Children utility methods. We saw two of them: React.Children.map to see how you can use it to make composite components, and React.Children.toArray in depth.
  2. We’ve seen React.Children.toArray change opaque children plug – which can be an object, array or function – in a flat array, so that one can work it in the required way – sort, filter, split, etc …
  3. We learned it React.Children.toArray does not run through React Fragments.
  4. We learned about an open-source package called react-keyed-flatten-children and understand how it solves the problem.
  5. We saw it Children utilities are in maintenance mode because they do not put together well.

You may also be interested in reading how you can use others Children methods to do everything you can do children in Max Stoiber’s blog post Respond children deep dive.

Resources

Smashing Editorial
(ks, vf, yk, il)





Source link