Map over Fragment in React 16
Asked Answered
P

2

6

I am trying to map over the children of a fragment that is in turn a child of a component. For example:

const Frag = () => (
  <React.Fragment key="test-key">
    <div>test1</div>
    <div>test2</div>
  </React.Fragment>
);

const Outer = ({ children }) => (
  <div>
    {
      React.Children.map(children, (child) => (
        <a>
          {child}
        </a>
      ))
    }
  </div>
);

// Usage
<Outer>
  <Frag />
</Outer>

This will result in a single a tag even though the fragment has multiple divs inside of it. The docs (https://reactjs.org/docs/react-api.html#reactchildrenmap) seem to indicate that this should work with a keyed fragment and I think I am creating a keyed fragment correctly (https://reactjs.org/docs/fragments.html#keyed-fragments). Any help would be appreciated!

Pepin answered 9/2, 2018 at 22:41 Comment(0)
U
4

Yes, but as far as I can see - you are iterating over Outer children. Try to

React.Children.map(children[0].props.children, (child) => (

But, looking closely at your links - I see that documentation says something wrong here

the function will never be passed the container objects

Here is an example, which clearly shows that container is passed to the function:

<script src="https://unpkg.com/@babel/standalone/babel.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="root" />
<script type="text/babel">
  const Frag = () => (
    <>
      <div>test1</div>
      <div>test2</div>
    </>
  ); 
  
  const Outer = ({ children }) => (
    <div>
      { 
        React.Children.map(children, (child) => console.log(`Child type is ${child.type.name || child.type.toString()}`) || (
        <a>
          {child}
        </a> )) 
      }
    </div>
  ); 
  
  
  ReactDOM.render((
    <div>
      <Outer>
        <>
          <div>test1</div>
          <div>test2</div>
        </>
      </Outer>
      <Outer>
        <Frag />
      </Outer>
    </div>
  ), document.getElementById('root'));
</script>
Undefined answered 9/2, 2018 at 22:47 Comment(2)
You're right that does work. Thanks! Just to clarify if I understood the first link correctly I shouldn't need to do this. It says that it should iterate over a keyed fragment. Do you have any insight into this?Pepin
It does render, but if you look at the resulting html both test1 and test2 are inside of a single a tag. I thought that given a fragment map children would return test1 and test2 inside their own a tags.Pepin
B
0

Instead of using React.Children.map, use this flatMapChildren function to flatten <>fragments</> as well as arrays:

import { Children } from 'react';

// TypeScript version
export function flatMapChildren<R>(children: React.ReactNode, fn: (x: React.ReactNode) => R) {
    let results: R[] = [];
    flatForEachChild(children, child => results.push(fn(child)));
    return results;
}

export function flatForEachChild(children: React.ReactNode, action: (x: React.ReactNode) => void) {
    if (isFragment(children)) {
        flatForEachChild(children.props.children, action);
    } else {
        Children.forEach(children, child => {
            if (isFragment(child))
                flatForEachChild(child.props.children, action);
            else
                action(child);
        });
    }
}

const REACT_FRAGMENT = Symbol.for('react.fragment');

export function isFragment(node: React.ReactNode): node is React.ReactElement<{ children: React.ReactNode }> {
    return (node as any)?.type === REACT_FRAGMENT;
}


// JavaScript version
export function flatForEachChild(children, action) {
    if (isFragment(children)) {
        flatForEachChild(children.props.children, action);
    } else {
        Children.forEach(children, child => {
            if (isFragment(child))
                flatForEachChild(child.props.children, action);
            else
                action(child);
        });
    }
}

export function flatMapChildren(children, fn) {
    let results = [];
    flatForEachChild(children, child => results.push(fn(child)));
    return results;
}

const REACT_FRAGMENT = Symbol.for('react.fragment');

export function isFragment(node) {
    return node != null && node.type === REACT_FRAGMENT;
}
Bud answered 12/5 at 20:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.